From bae7564968486451e5e2f5abfc6a5c29dfba37ae Mon Sep 17 00:00:00 2001 From: Kirill Sizov Date: Wed, 28 Sep 2022 22:56:50 +0300 Subject: [PATCH] Add webhooks (#4863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: “klakhov” Co-authored-by: Boris Co-authored-by: kirill-sizov --- .eslintrc.js | 2 +- .vscode/launch.json | 20 + cvat-core/src/api-implementation.ts | 37 +- cvat-core/src/api.ts | 25 +- cvat-core/src/comment.ts | 3 +- cvat-core/src/enums.ts | 26 + cvat-core/src/issue.ts | 3 +- cvat-core/src/organization.ts | 3 +- cvat-core/src/project.ts | 2 +- cvat-core/src/server-proxy.ts | 174 +- cvat-core/src/session.ts | 2 +- cvat-core/src/user.ts | 360 +-- cvat-core/src/webhook.ts | 351 +++ cvat-core/tests/api/user.js | 2 +- cvat-core/tests/api/webhooks.js | 124 + cvat-core/tests/mocks/dummy-data.mock.js | 165 +- cvat-core/tests/mocks/server-proxy.mock.js | 78 + cvat-ui/src/actions/webhooks-actions.ts | 113 + cvat-ui/src/components/cvat-app.tsx | 8 + cvat-ui/src/components/header/header.tsx | 4 +- .../components/organization-page/styles.scss | 5 + .../components/organization-page/top-bar.tsx | 128 +- .../components/projects-page/actions-menu.tsx | 16 + .../resource-sorting-filtering/filtering.tsx | 102 +- .../create-webhook-page.tsx | 48 + .../setup-webhook-content.tsx | 312 +++ .../setup-webhook-pages/styles.scss | 22 + .../update-webhook-page.tsx | 52 + .../components/webhooks-page/empty-list.tsx | 35 + .../src/components/webhooks-page/styles.scss | 136 + .../src/components/webhooks-page/top-bar.tsx | 94 + .../components/webhooks-page/webhook-item.tsx | 197 ++ .../webhooks-filter-configuration.ts | 55 + .../webhooks-page/webhooks-list.tsx | 29 + .../webhooks-page/webhooks-page.tsx | 151 ++ cvat-ui/src/cvat-core-wrapper.ts | 2 + cvat-ui/src/reducers/index.ts | 24 + cvat-ui/src/reducers/notifications-reducer.ts | 71 + cvat-ui/src/reducers/root-reducer.ts | 2 + cvat-ui/src/reducers/webhooks-reducer.ts | 56 + cvat/apps/engine/mixins.py | 23 + cvat/apps/engine/models.py | 15 + cvat/apps/engine/views.py | 33 +- cvat/apps/iam/permissions.py | 97 + cvat/apps/iam/rules/utils.rego | 1 + cvat/apps/iam/rules/webhooks.csv | 21 + cvat/apps/iam/rules/webhooks.rego | 173 ++ cvat/apps/iam/rules/webhooks_test.gen.rego | 2206 +++++++++++++++++ cvat/apps/organizations/models.py | 8 + cvat/apps/organizations/views.py | 26 +- cvat/apps/webhooks/__init__.py | 0 cvat/apps/webhooks/apps.py | 12 + cvat/apps/webhooks/event_type.py | 58 + cvat/apps/webhooks/migrations/0001_initial.py | 64 + .../0002_alter_webhookdelivery_status_code.py | 18 + cvat/apps/webhooks/migrations/__init__.py | 0 cvat/apps/webhooks/models.py | 107 + cvat/apps/webhooks/serializers.py | 152 ++ cvat/apps/webhooks/signals.py | 221 ++ cvat/apps/webhooks/urls.py | 11 + cvat/apps/webhooks/views.py | 195 ++ cvat/settings/base.py | 7 + cvat/urls.py | 3 + docker-compose.yml | 21 + package.json | 1 - supervisord/all.conf | 6 + supervisord/worker.webhooks.conf | 36 + tests/.eslintrc.js | 2 +- tests/cypress.json | 1 + tests/cypress/integration/webhooks.js | 105 + tests/cypress/support/commands_webhooks.js | 81 + tests/cypress/support/index.js | 1 + tests/docker-compose.webhook.yml | 17 + tests/python/rest_api/test_projects.py | 112 +- tests/python/rest_api/test_webhooks.py | 1052 ++++++++ tests/python/rest_api/test_webhooks_sender.py | 98 + tests/python/shared/assets/cvat_db/data.json | 301 ++- tests/python/shared/assets/invitations.json | 22 +- tests/python/shared/assets/memberships.json | 17 +- tests/python/shared/assets/projects.json | 102 +- tests/python/shared/assets/users.json | 10 +- tests/python/shared/assets/webhooks.json | 138 ++ tests/python/shared/fixtures/data.py | 5 + tests/python/shared/fixtures/init.py | 14 +- tests/python/shared/utils/dump_objects.py | 2 +- tests/python/webhook_receiver/.env | 2 + tests/python/webhook_receiver/server.py | 33 + yarn.lock | 5 - 88 files changed, 8294 insertions(+), 380 deletions(-) create mode 100644 cvat-core/src/webhook.ts create mode 100644 cvat-core/tests/api/webhooks.js create mode 100644 cvat-ui/src/actions/webhooks-actions.ts create mode 100644 cvat-ui/src/components/setup-webhook-pages/create-webhook-page.tsx create mode 100644 cvat-ui/src/components/setup-webhook-pages/setup-webhook-content.tsx create mode 100644 cvat-ui/src/components/setup-webhook-pages/styles.scss create mode 100644 cvat-ui/src/components/setup-webhook-pages/update-webhook-page.tsx create mode 100644 cvat-ui/src/components/webhooks-page/empty-list.tsx create mode 100644 cvat-ui/src/components/webhooks-page/styles.scss create mode 100644 cvat-ui/src/components/webhooks-page/top-bar.tsx create mode 100644 cvat-ui/src/components/webhooks-page/webhook-item.tsx create mode 100644 cvat-ui/src/components/webhooks-page/webhooks-filter-configuration.ts create mode 100644 cvat-ui/src/components/webhooks-page/webhooks-list.tsx create mode 100644 cvat-ui/src/components/webhooks-page/webhooks-page.tsx create mode 100644 cvat-ui/src/reducers/webhooks-reducer.ts create mode 100644 cvat/apps/iam/rules/webhooks.csv create mode 100644 cvat/apps/iam/rules/webhooks.rego create mode 100644 cvat/apps/iam/rules/webhooks_test.gen.rego create mode 100644 cvat/apps/webhooks/__init__.py create mode 100644 cvat/apps/webhooks/apps.py create mode 100644 cvat/apps/webhooks/event_type.py create mode 100644 cvat/apps/webhooks/migrations/0001_initial.py create mode 100644 cvat/apps/webhooks/migrations/0002_alter_webhookdelivery_status_code.py create mode 100644 cvat/apps/webhooks/migrations/__init__.py create mode 100644 cvat/apps/webhooks/models.py create mode 100644 cvat/apps/webhooks/serializers.py create mode 100644 cvat/apps/webhooks/signals.py create mode 100644 cvat/apps/webhooks/urls.py create mode 100644 cvat/apps/webhooks/views.py create mode 100644 supervisord/worker.webhooks.conf create mode 100644 tests/cypress/integration/webhooks.js create mode 100644 tests/cypress/support/commands_webhooks.js create mode 100644 tests/docker-compose.webhook.yml create mode 100644 tests/python/rest_api/test_webhooks.py create mode 100644 tests/python/rest_api/test_webhooks_sender.py create mode 100644 tests/python/shared/assets/webhooks.json create mode 100644 tests/python/webhook_receiver/.env create mode 100644 tests/python/webhook_receiver/server.py diff --git a/.eslintrc.js b/.eslintrc.js index 20119f95da96..152821a7d190 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,7 +17,7 @@ module.exports = { '.eslintrc.js', 'lint-staged.config.js', ], - plugins: ['@typescript-eslint', 'security', 'no-unsanitized', 'eslint-plugin-header', 'import'], + plugins: ['@typescript-eslint', 'security', 'no-unsanitized', 'import'], extends: [ 'eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM', 'airbnb-base', 'plugin:import/errors', 'plugin:import/warnings', diff --git a/.vscode/launch.json b/.vscode/launch.json index c5bb87012de8..7c9e55bac603 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -124,6 +124,25 @@ "env": {}, "console": "internalConsole" }, + { + "name": "server: RQ - webhooks", + "type": "python", + "request": "launch", + "justMyCode": false, + "stopOnEntry": false, + "python": "${command:python.interpreterPath}", + "program": "${workspaceRoot}/manage.py", + "args": [ + "rqworker", + "webhooks", + "--worker-class", + "cvat.simpleworker.SimpleWorker", + ], + "django": true, + "cwd": "${workspaceFolder}", + "env": {}, + "console": "internalConsole" + }, { "name": "server: git", "type": "python", @@ -285,6 +304,7 @@ "server: django", "server: RQ - default", "server: RQ - low", + "server: RQ - webhooks", "server: RQ - scheduler", "server: git", ] diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 2a9221e213b8..56500fe1e91f 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -17,13 +18,14 @@ const config = require('./config'); checkObjectType, } = require('./common'); - const User = require('./user'); + const User = require('./user').default; const { AnnotationFormats } = require('./annotation-formats'); const { ArgumentError } = require('./exceptions'); const { Task, Job } = require('./session'); const Project = require('./project').default; const { CloudStorage } = require('./cloud-storage'); const Organization = require('./organization'); + const Webhook = require('./webhook').default; function implementAPI(cvat) { cvat.plugins.list.implementation = PluginRegistry.list; @@ -286,6 +288,39 @@ const config = require('./config'); config.organizationID = null; }; + cvat.webhooks.get.implementation = async (filter) => { + checkFilter(filter, { + page: isInteger, + id: isInteger, + projectId: isInteger, + filter: isString, + search: isString, + sort: isString, + }); + + checkExclusiveFields(filter, ['id', 'projectId'], ['page']); + const searchParams = {}; + for (const key of Object.keys(filter)) { + if (['page', 'id', 'filter', 'search', 'sort'].includes(key)) { + searchParams[key] = filter[key]; + } + } + + if (filter.projectId) { + if (searchParams.filter) { + const parsed = JSON.parse(searchParams.filter); + searchParams.filter = JSON.stringify({ and: [parsed, { '==': [{ var: 'project_id' }, filter.projectId] }] }); + } else { + searchParams.filter = JSON.stringify({ and: [{ '==': [{ var: 'project_id' }, filter.projectId] }] }); + } + } + + const webhooksData = await serverProxy.webhooks.get(searchParams); + const webhooks = webhooksData.map((webhookData) => new Webhook(webhookData)); + webhooks.count = webhooksData.count; + return webhooks; + }; + return cvat; } diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index 59bb62046779..5837cf301fdf 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -23,6 +24,7 @@ function build() { const { FrameData } = require('./frames'); const { CloudStorage } = require('./cloud-storage'); const Organization = require('./organization'); + const Webhook = require('./webhook').default; const enums = require('./enums'); @@ -30,7 +32,7 @@ function build() { Exception, ArgumentError, DataError, ScriptingError, PluginError, ServerError, } = require('./exceptions'); - const User = require('./user'); + const User = require('./user').default; const pjson = require('../package.json'); const config = require('./config'); @@ -843,6 +845,26 @@ function build() { return result; }, }, + /** + * This namespace could be used to get webhooks list from the server + * @namespace webhooks + * @memberof module:API.cvat + */ + webhooks: { + /** + * Method returns a list of organizations + * @method get + * @async + * @memberof module:API.cvat.webhooks + * @returns {module:API.cvat.classes.Webhook[]} + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + */ + async get(filter: any) { + const result = await PluginRegistry.apiWrapper(cvat.webhooks.get, filter); + return result; + }, + }, /** * Namespace is used for access to classes * @namespace classes @@ -864,6 +886,7 @@ function build() { FrameData, CloudStorage, Organization, + Webhook, }, }; diff --git a/cvat-core/src/comment.ts b/cvat-core/src/comment.ts index 8703dd504897..e25073095954 100644 --- a/cvat-core/src/comment.ts +++ b/cvat-core/src/comment.ts @@ -1,8 +1,9 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -const User = require('./user'); +const User = require('./user').default; const { ArgumentError } = require('./exceptions'); /** diff --git a/cvat-core/src/enums.ts b/cvat-core/src/enums.ts index 1f1ebd982ee4..2508625644ca 100644 --- a/cvat-core/src/enums.ts +++ b/cvat-core/src/enums.ts @@ -438,3 +438,29 @@ export enum StorageLocation { LOCAL = 'local', CLOUD_STORAGE = 'cloud_storage', } + +/** + * Webhook source types + * @enum {string} + * @name WebhookSourceType + * @memberof module:API.cvat.enums + * @property {string} ORGANIZATION 'organization' + * @property {string} PROJECT 'project' + * @readonly +*/ +export enum WebhookSourceType { + ORGANIZATION = 'organization', + PROJECT = 'project', +} + +/** + * Webhook content types + * @enum {string} + * @name WebhookContentType + * @memberof module:API.cvat.enums + * @property {string} JSON 'json' + * @readonly +*/ +export enum WebhookContentType { + JSON = 'application/json', +} diff --git a/cvat-core/src/issue.ts b/cvat-core/src/issue.ts index 9d6b5c2b6dfb..27148b388343 100644 --- a/cvat-core/src/issue.ts +++ b/cvat-core/src/issue.ts @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,7 +7,7 @@ const quickhull = require('quickhull'); const PluginRegistry = require('./plugins').default; const Comment = require('./comment'); -const User = require('./user'); +const User = require('./user').default; const { ArgumentError } = require('./exceptions'); const serverProxy = require('./server-proxy').default; diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index 4a304ca550e4..a1765862c54e 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -8,7 +9,7 @@ const { MembershipRole } = require('./enums'); const { ArgumentError, ServerError } = require('./exceptions'); const PluginRegistry = require('./plugins').default; const serverProxy = require('./server-proxy').default; -const User = require('./user'); +const User = require('./user').default; /** * Class representing an organization diff --git a/cvat-core/src/project.ts b/cvat-core/src/project.ts index 4e4cede062bb..f50ddbe8e6ff 100644 --- a/cvat-core/src/project.ts +++ b/cvat-core/src/project.ts @@ -9,7 +9,7 @@ import { Storage } from './storage'; const PluginRegistry = require('./plugins').default; const { ArgumentError } = require('./exceptions'); const { Label } = require('./labels'); -const User = require('./user'); +const User = require('./user').default; const { FieldUpdateTrigger } = require('./common'); /** diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index de7b3de43d3b..f78bc9231b49 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: MIT -import { StorageLocation } from './enums'; +import { StorageLocation, WebhookSourceType } from './enums'; import { Storage } from './storage'; type Params = { @@ -18,12 +18,11 @@ type Params = { const FormData = require('form-data'); const store = require('store'); +const Axios = require('axios'); +const tus = require('tus-js-client'); const config = require('./config'); const DownloadWorker = require('./download.worker'); const { ServerError } = require('./exceptions'); -const Axios = require('axios'); -const tus = require('tus-js-client'); - function enableOrganization() { return { org: config.organizationID || '' }; @@ -921,8 +920,8 @@ class ServerProxy { } setTimeout(request); - }) - }; + }); + } const isCloudStorage = storage.location === StorageLocation.CLOUD_STORAGE; @@ -2022,11 +2021,160 @@ class ServerProxy { response = await Axios.get(`${backendAPI}/invitations/${id}`, { proxy: config.proxy, }); + return response.data; } catch (errorData) { throw generateError(errorData); } + } - return response.data; + async function getWebhookDelivery(webhookID: number, deliveryID: number): Promise { + const params = enableOrganization(); + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/webhooks/${webhookID}/deliveries/${deliveryID}`, { + proxy: config.proxy, + params, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); + } + } + + async function getWebhooks(filter, pageSize = 10): Promise { + const params = enableOrganization(); + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/webhooks`, { + proxy: config.proxy, + params: { + ...params, + ...filter, + page_size: pageSize, + }, + headers: { + 'Content-Type': 'application/json', + }, + }); + + response.data.results.count = response.data.count; + return response.data.results; + } catch (errorData) { + throw generateError(errorData); + } + } + + async function createWebhook(webhookData: any): Promise { + const params = enableOrganization(); + const { backendAPI } = config; + + try { + const response = await Axios.post(`${backendAPI}/webhooks`, JSON.stringify(webhookData), { + proxy: config.proxy, + params, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); + } + } + + async function updateWebhook(webhookID: number, webhookData: any): Promise { + const params = enableOrganization(); + const { backendAPI } = config; + + try { + const response = await Axios + .patch(`${backendAPI}/webhooks/${webhookID}`, JSON.stringify(webhookData), { + proxy: config.proxy, + params, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); + } + } + + async function deleteWebhook(webhookID: number): Promise { + const params = enableOrganization(); + const { backendAPI } = config; + + try { + await Axios.delete(`${backendAPI}/webhooks/${webhookID}`, { + proxy: config.proxy, + params, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); + } + } + + async function pingWebhook(webhookID: number): Promise { + const params = enableOrganization(); + const { backendAPI } = config; + + async function waitPingDelivery(deliveryID: number): Promise { + return new Promise((resolve) => { + async function checkStatus(): Promise { + const delivery = await getWebhookDelivery(webhookID, deliveryID); + if (delivery.status_code) { + resolve(delivery); + } else { + setTimeout(checkStatus, 1000); + } + } + setTimeout(checkStatus, 1000); + }); + } + + try { + const response = await Axios.post(`${backendAPI}/webhooks/${webhookID}/ping`, { + proxy: config.proxy, + params, + headers: { + 'Content-Type': 'application/json', + }, + }); + + const deliveryID = response.data.id; + const delivery = await waitPingDelivery(deliveryID); + return delivery; + } catch (errorData) { + throw generateError(errorData); + } + } + + async function receiveWebhookEvents(type: WebhookSourceType): Promise { + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/webhooks/events`, { + proxy: config.proxy, + params: { + type, + }, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data.events; + } catch (errorData) { + throw generateError(errorData); + } } Object.defineProperties( @@ -2189,6 +2337,18 @@ class ServerProxy { }), writable: false, }, + + webhooks: { + value: Object.freeze({ + get: getWebhooks, + create: createWebhook, + update: updateWebhook, + delete: deleteWebhook, + ping: pingWebhook, + events: receiveWebhookEvents, + }), + writable: false, + }, }), ); } diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 601a157f31b7..377f9797c198 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -26,7 +26,7 @@ const { JobStage, JobState, HistoryActions, } = require('./enums'); const { Label } = require('./labels'); -const User = require('./user'); +const User = require('./user').default; const Issue = require('./issue'); const { FieldUpdateTrigger, checkObjectType } = require('./common'); diff --git a/cvat-core/src/user.ts b/cvat-core/src/user.ts index 74f128b40a11..540f72a72085 100644 --- a/cvat-core/src/user.ts +++ b/cvat-core/src/user.ts @@ -1,180 +1,200 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -(() => { - /** - * Class representing a user - * @memberof module:API.cvat.classes - * @hideconstructor - */ - class User { - constructor(initialData) { - const data = { - id: null, - username: null, - email: null, - first_name: null, - last_name: null, - groups: null, - last_login: null, - date_joined: null, - is_staff: null, - is_superuser: null, - is_active: null, - email_verification_required: null, - }; +interface RawUserData { + id: number; + username: string; + email: string; + first_name: string; + last_name: string; + groups: string[]; + last_login: string; + date_joined: string; + is_staff: boolean; + is_superuser: boolean; + is_active: boolean; + email_verification_required: boolean; +} - for (const property in data) { - if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { - data[property] = initialData[property]; - } - } +export default class User { + public readonly id: number; + public readonly username: string; + public readonly email: string; + public readonly firstName: string; + public readonly lastName: string; + public readonly groups: string[]; + public readonly lastLogin: string; + public readonly dateJoined: string; + public readonly isStaff: boolean; + public readonly isSuperuser: boolean; + public readonly isActive: boolean; + public readonly isVerified: boolean; - Object.defineProperties( - this, - Object.freeze({ - id: { - /** - * @name id - * @type {number} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.id, - }, - username: { - /** - * @name username - * @type {string} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.username, - }, - email: { - /** - * @name email - * @type {string} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.email, - }, - firstName: { - /** - * @name firstName - * @type {string} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.first_name, - }, - lastName: { - /** - * @name lastName - * @type {string} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.last_name, - }, - groups: { - /** - * @name groups - * @type {string[]} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => JSON.parse(JSON.stringify(data.groups)), - }, - lastLogin: { - /** - * @name lastLogin - * @type {string} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.last_login, - }, - dateJoined: { - /** - * @name dateJoined - * @type {string} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.date_joined, - }, - isStaff: { - /** - * @name isStaff - * @type {boolean} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.is_staff, - }, - isSuperuser: { - /** - * @name isSuperuser - * @type {boolean} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.is_superuser, - }, - isActive: { - /** - * @name isActive - * @type {boolean} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => data.is_active, - }, - isVerified: { - /** - * @name isVerified - * @type {boolean} - * @memberof module:API.cvat.classes.User - * @readonly - * @instance - */ - get: () => !data.email_verification_required, - }, - }), - ); - } + constructor(initialData: RawUserData) { + const data = { + id: null, + username: null, + email: null, + first_name: null, + last_name: null, + groups: null, + last_login: null, + date_joined: null, + is_staff: null, + is_superuser: null, + is_active: null, + email_verification_required: null, + }; - serialize() { - return { - id: this.id, - username: this.username, - email: this.email, - first_name: this.firstName, - last_name: this.lastName, - groups: this.groups, - last_login: this.lastLogin, - date_joined: this.dateJoined, - is_staff: this.isStaff, - is_superuser: this.isSuperuser, - is_active: this.isActive, - email_verification_required: this.isVerified, - }; + for (const property in data) { + if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { + data[property] = initialData[property]; + } } + + Object.defineProperties( + this, + Object.freeze({ + id: { + /** + * @name id + * @type {number} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.id, + }, + username: { + /** + * @name username + * @type {string} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.username, + }, + email: { + /** + * @name email + * @type {string} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.email, + }, + firstName: { + /** + * @name firstName + * @type {string} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.first_name, + }, + lastName: { + /** + * @name lastName + * @type {string} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.last_name, + }, + groups: { + /** + * @name groups + * @type {string[]} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => JSON.parse(JSON.stringify(data.groups)), + }, + lastLogin: { + /** + * @name lastLogin + * @type {string} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.last_login, + }, + dateJoined: { + /** + * @name dateJoined + * @type {string} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.date_joined, + }, + isStaff: { + /** + * @name isStaff + * @type {boolean} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.is_staff, + }, + isSuperuser: { + /** + * @name isSuperuser + * @type {boolean} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.is_superuser, + }, + isActive: { + /** + * @name isActive + * @type {boolean} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => data.is_active, + }, + isVerified: { + /** + * @name isVerified + * @type {boolean} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => !data.email_verification_required, + }, + }), + ); } - module.exports = User; -})(); + serialize(): RawUserData { + return { + id: this.id, + username: this.username, + email: this.email, + first_name: this.firstName, + last_name: this.lastName, + groups: this.groups, + last_login: this.lastLogin, + date_joined: this.dateJoined, + is_staff: this.isStaff, + is_superuser: this.isSuperuser, + is_active: this.isActive, + email_verification_required: this.isVerified, + }; + } +} diff --git a/cvat-core/src/webhook.ts b/cvat-core/src/webhook.ts new file mode 100644 index 000000000000..09424cfff009 --- /dev/null +++ b/cvat-core/src/webhook.ts @@ -0,0 +1,351 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import PluginRegistry from './plugins'; +import User from './user'; +import serverProxy from './server-proxy'; +import { WebhookSourceType, WebhookContentType } from './enums'; +import { isEnum } from './common'; + +interface RawWebhookData { + id?: number; + type: WebhookSourceType; + target_url: string; + organization_id?: number; + project_id?: number; + events: string[]; + content_type: WebhookContentType; + secret?: string; + enable_ssl: boolean; + description?: string; + is_active?: boolean; + owner?: any; + created_date?: string; + updated_date?: string; + last_delivery_date?: string; + last_status?: number; +} + +export default class Webhook { + public readonly id: number; + public readonly type: WebhookSourceType; + public readonly organizationID: number | null; + public readonly projectID: number | null; + public readonly owner: User; + public readonly lastStatus: number; + public readonly lastDeliveryDate?: string; + public readonly createdDate: string; + public readonly updatedDate: string; + + public targetURL: string; + public events: string[]; + public contentType: RawWebhookData['content_type']; + public description?: string; + public secret?: string; + public isActive?: boolean; + public enableSSL: boolean; + + static async availableEvents(type: WebhookSourceType): Promise { + return serverProxy.webhooks.events(type); + } + + constructor(initialData: RawWebhookData) { + const data: RawWebhookData = { + id: undefined, + target_url: '', + type: WebhookSourceType.ORGANIZATION, + events: [], + content_type: WebhookContentType.JSON, + organization_id: null, + project_id: null, + description: undefined, + secret: '', + is_active: undefined, + enable_ssl: undefined, + owner: undefined, + created_date: undefined, + updated_date: undefined, + last_delivery_date: undefined, + last_status: 0, + }; + + for (const property in data) { + if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { + data[property] = initialData[property]; + } + } + + if (data.owner) { + data.owner = new User(data.owner); + } + + Object.defineProperties( + this, + Object.freeze({ + id: { + get: () => data.id, + }, + type: { + get: () => data.type, + }, + targetURL: { + get: () => data.target_url, + set: (value: string) => { + if (typeof value !== 'string') { + throw ArgumentError( + `targetURL property must be a string, tried to set ${typeof value}`, + ); + } + data.target_url = value; + }, + }, + events: { + get: () => data.events, + set: (events: string[]) => { + if (!Array.isArray(events)) { + throw ArgumentError( + `Events must be an array, tried to set ${typeof events}`, + ); + } + events.forEach((event: string) => { + if (typeof event !== 'string') { + throw ArgumentError( + `Event must be a string, tried to set ${typeof event}`, + ); + } + }); + data.events = [...events]; + }, + }, + contentType: { + get: () => data.content_type, + set: (value: WebhookContentType) => { + if (!isEnum.call(WebhookContentType, value)) { + throw new ArgumentError( + `Webhook contentType must be member of WebhookContentType, + got wrong value ${typeof value}`, + ); + } + data.content_type = value; + }, + }, + organizationID: { + get: () => data.organization_id, + }, + projectID: { + get: () => data.project_id, + }, + description: { + get: () => data.description, + set: (value: string) => { + if (typeof value !== 'string') { + throw ArgumentError( + `Description property must be a string, tried to set ${typeof value}`, + ); + } + data.description = value; + }, + }, + secret: { + get: () => data.secret, + set: (value: string) => { + if (typeof value !== 'string') { + throw ArgumentError( + `Secret property must be a string, tried to set ${typeof value}`, + ); + } + data.secret = value; + }, + }, + isActive: { + get: () => data.is_active, + set: (value: boolean) => { + if (typeof value !== 'boolean') { + throw ArgumentError( + `isActive property must be a boolean, tried to set ${typeof value}`, + ); + } + data.is_active = value; + }, + }, + enableSSL: { + get: () => data.enable_ssl, + set: (value: boolean) => { + if (typeof value !== 'boolean') { + throw ArgumentError( + `enableSSL property must be a boolean, tried to set ${typeof value}`, + ); + } + data.enable_ssl = value; + }, + }, + owner: { + get: () => data.owner, + }, + createdDate: { + get: () => data.created_date, + }, + updatedDate: { + get: () => data.updated_date, + }, + lastDeliveryDate: { + get: () => data.last_delivery_date, + }, + lastStatus: { + get: () => data.last_status, + }, + }), + ); + } + + public toJSON(): RawWebhookData { + const result: RawWebhookData = { + target_url: this.targetURL, + events: [...this.events], + content_type: this.contentType, + enable_ssl: this.enableSSL, + type: this.type || WebhookSourceType.ORGANIZATION, + }; + + if (Number.isInteger(this.id)) { + result.id = this.id; + } + + if (Number.isInteger(this.organizationID)) { + result.organization_id = this.organizationID; + } + + if (Number.isInteger(this.projectID)) { + result.project_id = this.projectID; + } + + if (this.description) { + result.description = this.description; + } + + if (this.secret) { + result.secret = this.secret; + } + + if (typeof this.isActive === 'boolean') { + result.is_active = this.isActive; + } + + return result; + } + + public async save(): Promise { + const result = await PluginRegistry.apiWrapper.call(this, Webhook.prototype.save); + return result; + } + + public async delete(): Promise { + const result = await PluginRegistry.apiWrapper.call(this, Webhook.prototype.delete); + return result; + } + + public async ping(): Promise { + const result = await PluginRegistry.apiWrapper.call(this, Webhook.prototype.ping); + return result; + } +} + +interface RawWebhookDeliveryData { + id?: number; + event?: string; + webhook_id?: number; + status_code?: string; + redelivery?: boolean; + created_date?: string; + updated_date?: string; +} + +export class WebhookDelivery { + public readonly id?: number; + public readonly event: string; + public readonly webhookId: number; + public readonly statusCode: string; + public readonly createdDate?: string; + public readonly updatedDate?: string; + + constructor(initialData: RawWebhookDeliveryData) { + const data: RawWebhookDeliveryData = { + id: undefined, + event: '', + webhook_id: undefined, + status_code: undefined, + created_date: undefined, + updated_date: undefined, + }; + + for (const property in data) { + if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { + data[property] = initialData[property]; + } + } + + Object.defineProperties( + this, + Object.freeze({ + id: { + get: () => data.id, + }, + event: { + get: () => data.event, + }, + webhookId: { + get: () => data.webhook_id, + }, + statusCode: { + get: () => data.status_code, + }, + createdDate: { + get: () => data.created_date, + }, + updatedDate: { + get: () => data.updated_date, + }, + }), + ); + } +} + +Object.defineProperties(Webhook.prototype.save, { + implementation: { + writable: false, + enumerable: false, + value: async function implementation() { + if (Number.isInteger(this.id)) { + const result = await serverProxy.webhooks.update(this.id, this.toJSON()); + return new Webhook(result); + } + + const result = await serverProxy.webhooks.create(this.toJSON()); + return new Webhook(result); + }, + }, +}); + +Object.defineProperties(Webhook.prototype.delete, { + implementation: { + writable: false, + enumerable: false, + value: async function implementation() { + if (Number.isInteger(this.id)) { + await serverProxy.webhooks.delete(this.id); + } + }, + }, +}); + +Object.defineProperties(Webhook.prototype.ping, { + implementation: { + writable: false, + enumerable: false, + value: async function implementation() { + const result = await serverProxy.webhooks.ping(this.id); + return new WebhookDelivery(result); + }, + }, +}); diff --git a/cvat-core/tests/api/user.js b/cvat-core/tests/api/user.js index 694aa61e9433..7d4e53ad1229 100644 --- a/cvat-core/tests/api/user.js +++ b/cvat-core/tests/api/user.js @@ -14,7 +14,7 @@ jest.mock('../../src/server-proxy', () => { // Initialize api window.cvat = require('../../src/api'); -const User = require('../../src/user'); +const User = require('../../src/user').default; // Test cases describe('Feature: get a list of users', () => { diff --git a/cvat-core/tests/api/webhooks.js b/cvat-core/tests/api/webhooks.js new file mode 100644 index 000000000000..0bee1010f04e --- /dev/null +++ b/cvat-core/tests/api/webhooks.js @@ -0,0 +1,124 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +// Setup mock for a server +jest.mock('../../src/server-proxy', () => { + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; +}); + +// Initialize api +window.cvat = require('../../src/api'); + +const Webhook = require('../../src/webhook').default; +const { webhooksDummyData, webhooksEventsDummyData } = require('../mocks/dummy-data.mock'); +const { WebhookSourceType } = require('../../src/enums'); + +describe('Feature: get webhooks', () => { + test('get all webhooks', async () => { + const result = await window.cvat.webhooks.get({}); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(webhooksDummyData.count); + for (const item of result) { + expect(item).toBeInstanceOf(Webhook); + } + }); + + test('get webhook events', async () => { + function checkEvents(events) { + expect(Array.isArray(result)).toBeTruthy(); + for (const event of events) { + expect(event).toMatch(/((create)|(update)|(delete)):/); + } + } + let result = await Webhook.availableEvents(WebhookSourceType.PROJECT); + checkEvents(result); + + result = await Webhook.availableEvents(WebhookSourceType.ORGANIZATION); + checkEvents(result); + }); + + test('get webhook by id', async () => { + const result = await window.cvat.webhooks.get({ + id: 1, + }); + const [webhook] = result; + + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(webhook).toBeInstanceOf(Webhook); + expect(webhook.id).toBe(1); + expect(webhook.targetURL).toBe('https://localhost:3001/project/hook'); + expect(webhook.description).toBe('Project webhook'); + expect(webhook.contentType).toBe('application/json'); + expect(webhook.enableSSL).toBeTruthy(); + expect(webhook.events).toEqual(webhooksEventsDummyData[WebhookSourceType.PROJECT].events); + }); +}); + + + +describe('Feature: create a webhook', () => { + test('create new webhook', async () => { + const webhook = new window.cvat.classes.Webhook({ + description: 'New webhook', + target_url: 'https://localhost:3001/hook', + content_type: 'application/json', + secret: 'secret', + enable_ssl: true, + is_active: true, + events: webhooksEventsDummyData[WebhookSourceType.PROJECT].events, + project_id: 1, + type:WebhookSourceType.PROJECT, + }); + + const result = await webhook.save(); + expect(typeof result.id).toBe('number'); + }); +}); + +describe('Feature: update a webhook', () => { + test('update some webhook fields', async () => { + const newValues = new Map([ + ['description', 'New description'], + ['isActive', false], + ['targetURL', 'https://localhost:3001/new/url'], + ]); + + let result = await window.cvat.webhooks.get({ + id: 1, + }); + let [webhook] = result; + for (const [key, value] of newValues) { + webhook[key] = value; + } + webhook.save(); + + result = await window.cvat.webhooks.get({ + id: 1, + }); + [webhook] = result; + newValues.forEach((value, key) => { + expect(webhook[key]).toBe(value); + }); + }); +}); + +describe('Feature: delete a webhook', () => { + test('delete a webhook', async () => { + let result = await window.cvat.webhooks.get({ + id: 2, + }); + const [webhook] = result; + await webhook.delete(); + + result = await window.cvat.webhooks.get({ + id: 2, + }); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + }); +}); diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index 2b4edfff1d2d..ef8b15d66213 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.js +++ b/cvat-core/tests/mocks/dummy-data.mock.js @@ -2723,7 +2723,7 @@ const taskAnnotationsDummyData = { ], id: 28, frame: 0, - label_id:59, + label_id: 59, group: 0, source: 'manual', attributes: [] @@ -2989,7 +2989,7 @@ const frameMetaDummyData = { start_frame: 0, stop_frame: 8, frame_filter: '', - deleted_frames: [7,8], + deleted_frames: [7, 8], frames: [ { width: 1920, @@ -3282,6 +3282,165 @@ const cloudStoragesDummyData = { ] }; +const webhooksDummyData = { + count: 3, + next: null, + previous: null, + results: [ + { + id: 1, + url: "http://localhost:7000/api/webhooks/1", + target_url: "https://localhost:3001/project/hook", + description: "Project webhook", + type: "project", + content_type: "application/json", + is_active: true, + enable_ssl: true, + created_date: "2022-09-23T06:29:12.337276Z", + updated_date: "2022-09-23T06:29:12.337316Z", + owner: { + url: "http://localhost:7000/api/users/1", + id: 1, + username: "kirill", + first_name: "", + last_name: "" + }, + project: 1, + organization: 1, + events: [ + "create:comment", + "create:issue", + "create:task", + "delete:comment", + "delete:issue", + "delete:task", + "update:comment", + "update:job", + "update:project", + "update:task" + ], + last_status: "Failed to connect to target url", + last_delivery_date: "2022-09-23T06:28:48.313010Z" + }, + { + id: 2, + url: "http://localhost:7000/api/webhooks/2", + target_url: "https://localhost:3001/example/route", + description: "Second webhook", + type: "organization", + content_type: "application/json", + is_active: true, + enable_ssl: true, + created_date: "2022-09-23T06:28:32.430437Z", + updated_date: "2022-09-23T06:28:32.430474Z", + owner: { + url: "http://localhost:7000/api/users/1", + id: 1, + username: "kirill", + first_name: "", + last_name: "" + }, + project: 1, + organization: 1, + events: [ + "create:project", + "create:task", + "delete:project", + "delete:task", + "update:job", + "update:project", + "update:task" + ], + last_status: "200", + last_delivery_date: "2022-09-23T06:28:48.313010Z" + }, + { + id: 3, + url: "http://localhost:7000/api/webhooks/3", + target_url: "https://localhost:3001/example1", + description: "Example webhook", + type: "organization", + content_type: "application/json", + is_active: true, + enable_ssl: true, + created_date: "2022-09-23T06:27:52.888204Z", + updated_date: "2022-09-23T06:27:52.888245Z", + owner: { + url: "http://localhost:7000/api/users/1", + id: 1, + username: "kirill", + first_name: "", + last_name: "" + }, + project: 1, + organization: 1, + events: [ + "create:comment", + "create:invitation", + "create:issue", + "create:project", + "create:task", + "delete:comment", + "delete:invitation", + "delete:issue", + "delete:membership", + "delete:project", + "delete:task", + "update:comment", + "update:invitation", + "update:job", + "update:membership", + "update:organization", + "update:project", + "update:task" + ], + last_status: "200", + last_delivery_date: "2022-09-23T06:28:48.283962Z" + } + ] +}; + +const webhooksEventsDummyData = { + project: { + webhook_type: "project", + events: [ + "create:comment", + "create:issue", + "create:task", + "delete:comment", + "delete:issue", + "delete:task", + "update:comment", + "update:job", + "update:project", + "update:task" + ] + }, + organization: { + webhook_type: "organization", + events: [ + "create:comment", + "create:invitation", + "create:issue", + "create:project", + "create:task", + "delete:comment", + "delete:invitation", + "delete:issue", + "delete:membership", + "delete:project", + "delete:task", + "update:comment", + "update:invitation", + "update:job", + "update:membership", + "update:organization", + "update:project", + "update:task" + ] + }, +} + module.exports = { tasksDummyData, projectsDummyData, @@ -3293,4 +3452,6 @@ module.exports = { frameMetaDummyData, formatsDummyData, cloudStoragesDummyData, + webhooksDummyData, + webhooksEventsDummyData, }; diff --git a/cvat-core/tests/mocks/server-proxy.mock.js b/cvat-core/tests/mocks/server-proxy.mock.js index 5344491b6ead..9491f9c0bcf6 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.js +++ b/cvat-core/tests/mocks/server-proxy.mock.js @@ -13,6 +13,8 @@ const { jobAnnotationsDummyData, frameMetaDummyData, cloudStoragesDummyData, + webhooksDummyData, + webhooksEventsDummyData, } = require('./dummy-data.mock'); function QueryStringToJSON(query, ignoreList = []) { @@ -412,6 +414,71 @@ class ServerProxy { } } + async function getWebhooks(filter = '') { + const queries = QueryStringToJSON(filter); + const result = webhooksDummyData.results.filter((item) => { + for (const key in queries) { + if (Object.prototype.hasOwnProperty.call(queries, key)) { + if (queries[key] !== item[key]) { + return false; + } + } + } + return true; + }); + return result; + } + + async function createWebhook(webhookData) { + const id = Math.max(...webhooksDummyData.results.map((item) => item.id)) + 1; + webhooksDummyData.results.push({ + id, + description: webhookData.description, + target_url: webhookData.target_url, + content_type: webhookData.content_type, + secret: webhookData.secret, + enable_ssl: webhookData.enable_ssl, + is_active: webhookData.is_active, + events: webhookData.events, + organization_id: webhookData.organization_id ? webhookData.organization_id : null, + project_id: webhookData.project_id ? webhookData.project_id : null, + type: webhookData.type, + owner: { id: 1 }, + created_date: '2022-09-23T06:29:12.337276Z', + updated_date: '2022-09-23T06:29:12.337276Z', + }); + + const result = await getWebhooks(`?id=${id}`); + return result[0]; + } + + async function updateWebhook(webhookID, webhookData) { + const webhook = webhooksDummyData.results.find((item) => item.id === webhookID); + if (webhook) { + for (const prop in webhookData) { + if ( + Object.prototype.hasOwnProperty.call(webhookData, prop) && + Object.prototype.hasOwnProperty.call(webhook, prop) + ) { + webhook[prop] = webhookData[prop]; + } + } + } + return webhook; + } + + async function receiveWebhookEvents(type) { + return webhooksEventsDummyData[type]?.events; + } + + async function deleteWebhook(webhookID) { + const webhooks = webhooksDummyData.results; + const webhookIdx = webhooks.findIndex((item) => item.id === webhookID); + if (webhookIdx !== -1) { + webhooks.splice(webhookIdx); + } + } + Object.defineProperties( this, Object.freeze({ @@ -489,6 +556,17 @@ class ServerProxy { }), writable: false, }, + + webhooks: { + value: Object.freeze({ + get: getWebhooks, + create: createWebhook, + update: updateWebhook, + delete: deleteWebhook, + events: receiveWebhookEvents, + }), + writable: false, + }, }), ); } diff --git a/cvat-ui/src/actions/webhooks-actions.ts b/cvat-ui/src/actions/webhooks-actions.ts new file mode 100644 index 000000000000..1b20b1ee41c7 --- /dev/null +++ b/cvat-ui/src/actions/webhooks-actions.ts @@ -0,0 +1,113 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { getCore, Webhook } from 'cvat-core-wrapper'; +import { Dispatch, ActionCreator, Store } from 'redux'; +import { Indexable, WebhooksQuery } from 'reducers'; +import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; + +const cvat = getCore(); + +export enum WebhooksActionsTypes { + GET_WEBHOOKS = 'GET_WEBHOOKS', + GET_WEBHOOKS_SUCCESS = 'GET_WEBHOOKS_SUCCESS', + GET_WEBHOOKS_FAILED = 'GET_WEBHOOKS_FAILED', + CREATE_WEBHOOK = 'CREATE_WEBHOOK', + CREATE_WEBHOOK_SUCCESS = 'CREATE_WEBHOOK_SUCCESS', + CREATE_WEBHOOK_FAILED = 'CREATE_WEBHOOK_FAILED', + UPDATE_WEBHOOK = 'UPDATE_WEBHOOK', + UPDATE_WEBHOOK_SUCCESS = 'UPDATE_WEBHOOK_SUCCESS', + UPDATE_WEBHOOK_FAILED = 'UPDATE_WEBHOOK_FAILED', + DELETE_WEBHOOK = 'DELETE_WEBHOOK', + DELETE_WEBHOOK_SUCCESS = 'DELETE_WEBHOOK_SUCCESS', + DELETE_WEBHOOK_FAILED = 'DELETE_WEBHOOK_FAILED', +} + +const webhooksActions = { + getWebhooks: (query: WebhooksQuery) => createAction(WebhooksActionsTypes.GET_WEBHOOKS, { query }), + getWebhooksSuccess: (webhooks: Webhook[], count: number) => createAction( + WebhooksActionsTypes.GET_WEBHOOKS_SUCCESS, { webhooks, count }, + ), + getWebhooksFailed: (error: any) => createAction(WebhooksActionsTypes.GET_WEBHOOKS_FAILED, { error }), + createWebhook: () => createAction(WebhooksActionsTypes.CREATE_WEBHOOK), + createWebhookSuccess: (webhook: Webhook) => createAction(WebhooksActionsTypes.CREATE_WEBHOOK_SUCCESS, { webhook }), + createWebhookFailed: (error: any) => createAction(WebhooksActionsTypes.CREATE_WEBHOOK_FAILED, { error }), + updateWebhook: () => createAction(WebhooksActionsTypes.UPDATE_WEBHOOK), + updateWebhookSuccess: (webhook: any) => createAction(WebhooksActionsTypes.UPDATE_WEBHOOK_SUCCESS, { webhook }), + updateWebhookFailed: (error: any) => createAction(WebhooksActionsTypes.UPDATE_WEBHOOK_FAILED, { error }), + deleteWebhook: () => createAction(WebhooksActionsTypes.DELETE_WEBHOOK), + deleteWebhookSuccess: () => createAction(WebhooksActionsTypes.DELETE_WEBHOOK_SUCCESS), + deleteWebhookFailed: (webhookID: number, error: any) => createAction( + WebhooksActionsTypes.DELETE_WEBHOOK_FAILED, { webhookID, error }, + ), +}; + +export const getWebhooksAsync = (query: WebhooksQuery): ThunkAction => ( + async (dispatch: ActionCreator): Promise => { + dispatch(webhooksActions.getWebhooks(query)); + + // We remove all keys with null values from the query + const filteredQuery = { ...query }; + for (const key of Object.keys(query)) { + if ((filteredQuery as Indexable)[key] === null) { + delete (filteredQuery as Indexable)[key]; + } + } + + let result = null; + try { + result = await cvat.webhooks.get(filteredQuery); + } catch (error) { + dispatch(webhooksActions.getWebhooksFailed(error)); + return; + } + + const array: Array = Array.from(result); + + dispatch(webhooksActions.getWebhooksSuccess(array, result.count)); + } +); + +export function createWebhookAsync(webhookData: Store): ThunkAction { + return async function (dispatch) { + const webhook = new cvat.classes.Webhook(webhookData); + dispatch(webhooksActions.createWebhook()); + + try { + const createdWebhook = await webhook.save(); + dispatch(webhooksActions.createWebhookSuccess(createdWebhook)); + } catch (error) { + dispatch(webhooksActions.createWebhookFailed(error)); + throw error; + } + }; +} + +export function updateWebhookAsync(webhook: Webhook): ThunkAction { + return async function (dispatch) { + dispatch(webhooksActions.updateWebhook()); + + try { + const updatedWebhook = await webhook.save(); + dispatch(webhooksActions.updateWebhookSuccess(updatedWebhook)); + } catch (error) { + dispatch(webhooksActions.updateWebhookFailed(error)); + throw error; + } + }; +} + +export function deleteWebhookAsync(webhook: Webhook): ThunkAction { + return async function (dispatch) { + try { + await webhook.delete(); + dispatch(webhooksActions.deleteWebhookSuccess()); + } catch (error) { + dispatch(webhooksActions.deleteWebhookFailed(webhook.id, error)); + throw error; + } + }; +} + +export type WebhooksActions = ActionUnion; diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index af0a6d9045ea..f38a0b01b9e5 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -48,6 +48,10 @@ import OrganizationPage from 'components/organization-page/organization-page'; import CreateOrganizationComponent from 'components/create-organization-page/create-organization-page'; import { ShortcutsContextProvider } from 'components/shortcuts.context'; +import WebhooksPage from 'components/webhooks-page/webhooks-page'; +import CreateWebhookPage from 'components/setup-webhook-pages/create-webhook-page'; +import UpdateWebhookPage from 'components/setup-webhook-pages/update-webhook-page'; + import AnnotationPageContainer from 'containers/annotation-page/annotation-page'; import { getCore } from 'cvat-core-wrapper'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; @@ -370,6 +374,7 @@ class CVATApplication extends React.PureComponent + @@ -391,6 +396,9 @@ class CVATApplication extends React.PureComponent + + + {isModelPluginActive && ( diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 97bec1749e69..03e97b76045c 100644 --- a/cvat-ui/src/components/header/header.tsx +++ b/cvat-ui/src/components/header/header.tsx @@ -227,7 +227,7 @@ function HeaderContainer(props: Props): JSX.Element { const resetOrganization = (): void => { localStorage.removeItem('currentOrganization'); - if (/\d+$/.test(window.location.pathname)) { + if (/(webhooks)|(\d+)/.test(window.location.pathname)) { window.location.pathname = '/'; } else { window.location.reload(); @@ -237,7 +237,7 @@ function HeaderContainer(props: Props): JSX.Element { const setNewOrganization = (organization: any): void => { if (!currentOrganization || currentOrganization.slug !== organization.slug) { localStorage.setItem('currentOrganization', organization.slug); - if (/\d+$/.test(window.location.pathname)) { + if (/\d+/.test(window.location.pathname)) { // a resource is opened (task/job/etc.) window.location.pathname = '/'; } else { diff --git a/cvat-ui/src/components/organization-page/styles.scss b/cvat-ui/src/components/organization-page/styles.scss index bb0f6a053e9d..cb96c3dd1063 100644 --- a/cvat-ui/src/components/organization-page/styles.scss +++ b/cvat-ui/src/components/organization-page/styles.scss @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -148,3 +149,7 @@ .cvat-organization-invitation-field { align-items: baseline; } + +.cvat-organization-page-actions-button { + padding-right: $grid-unit-size * 0.5; +} diff --git a/cvat-ui/src/components/organization-page/top-bar.tsx b/cvat-ui/src/components/organization-page/top-bar.tsx index f2c84c8c98b9..b92dd8cab216 100644 --- a/cvat-ui/src/components/organization-page/top-bar.tsx +++ b/cvat-ui/src/components/organization-page/top-bar.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -13,11 +14,14 @@ import Space from 'antd/lib/space'; import Input from 'antd/lib/input'; import Form from 'antd/lib/form'; import Select from 'antd/lib/select'; +import Dropdown from 'antd/lib/dropdown'; +import Menu from 'antd/lib/menu'; import { useForm } from 'antd/lib/form/Form'; import { Store } from 'antd/lib/form/interface'; + import { EditTwoTone, EnvironmentOutlined, - MailOutlined, PhoneOutlined, PlusCircleOutlined, DeleteOutlined, + MailOutlined, PhoneOutlined, PlusCircleOutlined, DeleteOutlined, MoreOutlined, } from '@ant-design/icons'; import { @@ -26,6 +30,7 @@ import { removeOrganizationAsync, updateOrganizationAsync, } from 'actions/organization-actions'; +import { useHistory } from 'react-router-dom'; export interface Props { organizationInstance: any; @@ -33,6 +38,11 @@ export interface Props { fetchMembers: () => void; } +export enum MenuActions { + SET_WEBHOOKS = 'SET_WEBHOOKS', + REMOVE_ORGANIZATION = 'REMOVE_ORGANIZATION', +} + function OrganizationTopBar(props: Props): JSX.Element { const { organizationInstance, userInstance, fetchMembers } = props; const { @@ -62,14 +72,86 @@ function OrganizationTopBar(props: Props): JSX.Element { let organizationName = name; let organizationDescription = description; let organizationContacts = contact; + const history = useHistory(); + return ( <>
- - {`Organization: ${slug} `} - + + + + {`Organization: ${slug} `} + + + + ( + + + { + e.preventDefault(); + history.push({ + pathname: '/organization/webhooks', + }); + return false; + }} + > + Setup webhooks + + + {owner && userID === owner.id ? ( + { + const modal = Modal.confirm({ + onOk: () => { + dispatch(removeOrganizationAsync(organizationInstance)); + }, + content: ( +
+ + To remove the organization, + enter its short name below + + ) => { + modal.update({ + okButtonProps: { + disabled: + event.target.value !== organizationInstance.slug, + danger: true, + }, + }); + }} + /> +
+ ), + okButtonProps: { + disabled: true, + danger: true, + }, + okText: 'Remove', + }); + }} + > + Remove organization +
+ ) : null} +
+ )} + > + +
+ + +
+ { @@ -213,44 +295,6 @@ function OrganizationTopBar(props: Props): JSX.Element { Leave organization ) : null} - {owner && userID === owner.id ? ( - - ) : null} - + setAppliedFilter({ + ...defaultAppliedFilter, + predefined: updatedValue, + }); + }} + key={key} + > + {key} + + )) } +
+ )} + > + + + ) : null + } + + + + + + + + + + + + ); +} + +export default React.memo(CreateWebhookPage); diff --git a/cvat-ui/src/components/setup-webhook-pages/setup-webhook-content.tsx b/cvat-ui/src/components/setup-webhook-pages/setup-webhook-content.tsx new file mode 100644 index 000000000000..c1d79e262fa4 --- /dev/null +++ b/cvat-ui/src/components/setup-webhook-pages/setup-webhook-content.tsx @@ -0,0 +1,312 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React, { useCallback, useEffect, useState } from 'react'; +import { Store } from 'antd/lib/form/interface'; +import { Row, Col } from 'antd/lib/grid'; +import Form from 'antd/lib/form'; +import Text from 'antd/lib/typography/Text'; +import Button from 'antd/lib/button'; +import Checkbox from 'antd/lib/checkbox/Checkbox'; +import Input from 'antd/lib/input'; +import Radio, { RadioChangeEvent } from 'antd/lib/radio'; +import Select from 'antd/lib/select'; +import notification from 'antd/lib/notification'; + +import { getCore, Webhook } from 'cvat-core-wrapper'; +import ProjectSearchField from 'components/create-task-page/project-search-field'; +import { useSelector, useDispatch } from 'react-redux'; +import { CombinedState } from 'reducers'; +import { createWebhookAsync, updateWebhookAsync } from 'actions/webhooks-actions'; + +export enum WebhookContentType { + APPLICATION_JSON = 'application/json', +} + +export enum WebhookSourceType { + ORGANIZATION = 'organization', + PROJECT = 'project', +} + +export enum EventsMethod { + SEND_EVERYTHING = 'SEND_EVERYTHING', + SELECT_INDIVIDUAL = 'SELECT_INDIVIDUAL', +} + +export interface SetupWebhookData { + description: string; + targetUrl: string; + contentType: WebhookContentType; + secret: string; + enableSSL: boolean; + active: boolean; + eventsMethod: EventsMethod; +} + +interface Props { + webhook?: any; + defaultProjectId: number | null; +} + +export function groupEvents(events: string[]): string[] { + return Array.from( + new Set(events.map((event: string) => event.split(':')[1])), + ); +} + +function collectEvents(method: EventsMethod, submittedGroups: Record, allEvents: string[]): string[] { + return method === EventsMethod.SEND_EVERYTHING ? allEvents : (() => { + const submittedEvents = Object.entries(submittedGroups).filter(([key, value]) => key.startsWith('event:') && value).map(([key]) => key) + .map((event: string) => event.split(':')[1]); + return allEvents.filter((event) => submittedEvents.includes(event.split(':')[1])); + })(); +} + +function SetupWebhookContent(props: Props): JSX.Element { + const dispatch = useDispatch(); + const { webhook, defaultProjectId } = props; + const [form] = Form.useForm(); + const [rerender, setRerender] = useState(false); + const [showDetailedEvents, setShowDetailedEvents] = useState(false); + const [webhookEvents, setWebhookEvents] = useState([]); + + const organization = useSelector((state: CombinedState) => state.organizations.current); + + const [projectId, setProjectId] = useState(defaultProjectId); + + useEffect(() => { + const core = getCore(); + if (webhook) { + core.classes.Webhook.availableEvents(webhook.type).then((events: string[]) => { + setWebhookEvents(events); + }); + } else { + core.classes.Webhook.availableEvents(projectId ? + WebhookSourceType.PROJECT : WebhookSourceType.ORGANIZATION).then((events: string[]) => { + setWebhookEvents(events); + }); + } + }, [projectId]); + + useEffect(() => { + if (webhook) { + const eventsMethod = groupEvents(webhookEvents).length === groupEvents(webhook.events).length ? + EventsMethod.SEND_EVERYTHING : EventsMethod.SELECT_INDIVIDUAL; + setShowDetailedEvents(eventsMethod === EventsMethod.SELECT_INDIVIDUAL); + const data: Record = { + description: webhook.description, + targetURL: webhook.targetURL, + contentType: webhook.contentType, + secret: webhook.secret, + enableSSL: webhook.enableSSL, + isActive: webhook.isActive, + events: webhook.events, + eventsMethod, + }; + + webhook.events.forEach((event: string) => { + data[`event:${event.split(':')[1]}`] = true; + }); + + form.setFieldsValue(data); + setRerender(!rerender); + } + }, [webhook, webhookEvents]); + + const handleSubmit = useCallback(async (): Promise => { + try { + const values: Store = await form.validateFields(); + let notificationConfig = { + message: 'Webhook has been successfully updated', + className: 'cvat-notification-update-webhook-success', + }; + if (webhook) { + webhook.description = values.description; + webhook.targetURL = values.targetURL; + webhook.secret = values.secret; + webhook.contentType = values.contentType; + webhook.isActive = values.isActive; + webhook.enableSSL = values.enableSSL; + webhook.events = collectEvents(values.eventsMethod, values, webhookEvents); + + await dispatch(updateWebhookAsync(webhook)); + } else { + const rawWebhookData = { + description: values.description, + target_url: values.targetURL, + content_type: values.contentType, + secret: values.secret, + enable_ssl: values.enableSSL, + is_active: values.isActive, + events: collectEvents(values.eventsMethod, values, webhookEvents), + organization_id: projectId ? undefined : organization.id, + project_id: projectId, + type: projectId ? WebhookSourceType.PROJECT : WebhookSourceType.ORGANIZATION, + }; + notificationConfig = { + message: 'Webhook has been successfully added', + className: 'cvat-notification-create-webhook-success', + }; + await dispatch(createWebhookAsync(rawWebhookData)); + } + form.resetFields(); + setShowDetailedEvents(false); + notification.info(notificationConfig); + return webhook; + } catch (error) { + return null; + } + }, [webhook, webhookEvents]); + + const onEventsMethodChange = useCallback((event: RadioChangeEvent): void => { + form.setFieldsValue({ eventsMethod: event.target.value }); + setShowDetailedEvents(event.target.value === EventsMethod.SELECT_INDIVIDUAL); + setRerender(!rerender); + }, [rerender]); + + return ( + + + Setup a webhook + + +
+ + + + + + + { + !webhook && ( + + + Project + + + setProjectId(_projectId)} + value={projectId} + /> + + + ) + } + + + + + + + + + + Enable SSL + + + + + Active + + + + + + Send + everything + + + Select individual events + + + + { + showDetailedEvents && ( + + {groupEvents(webhookEvents).map((event: string, idx: number) => ( + + + + {event} + + + + ))} + + + ) + } +
+ + + + + + + + +
+ + ); +} + +export default React.memo(SetupWebhookContent); diff --git a/cvat-ui/src/components/setup-webhook-pages/styles.scss b/cvat-ui/src/components/setup-webhook-pages/styles.scss new file mode 100644 index 000000000000..5b8fe7d0cc47 --- /dev/null +++ b/cvat-ui/src/components/setup-webhook-pages/styles.scss @@ -0,0 +1,22 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-setup-webhook-content { + margin-top: $grid-unit-size; + width: 100%; + height: auto; + border: 1px solid $border-color-1; + border-radius: 3px; + padding: $grid-unit-size * 3; + background: $background-color-1; + text-align: initial; +} + +.cvat-create-webhook-page { + width: 100%; + height: 100%; + padding-top: $grid-unit-size * 5; +} diff --git a/cvat-ui/src/components/setup-webhook-pages/update-webhook-page.tsx b/cvat-ui/src/components/setup-webhook-pages/update-webhook-page.tsx new file mode 100644 index 000000000000..e3cbae65cdda --- /dev/null +++ b/cvat-ui/src/components/setup-webhook-pages/update-webhook-page.tsx @@ -0,0 +1,52 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useEffect } from 'react'; +import Button from 'antd/lib/button'; +import { Row, Col } from 'antd/lib/grid'; +import { LeftOutlined } from '@ant-design/icons'; +import { useHistory, useParams } from 'react-router'; +import { CombinedState } from 'reducers'; +import { useDispatch, useSelector } from 'react-redux'; +import { getWebhooksAsync } from 'actions/webhooks-actions'; +import SetupWebhookContent from './setup-webhook-content'; + +interface ParamType { + id: string; +} + +function UpdateWebhookPage(): JSX.Element { + const id = +useParams().id; + const history = useHistory(); + const dispatch = useDispatch(); + const webhooks = useSelector((state: CombinedState) => state.webhooks.current); + const [webhook] = webhooks.filter((_webhook) => _webhook.id === id); + + useEffect(() => { + if (!webhook) { + dispatch(getWebhooksAsync({ id })); + } + }, []); + + return ( +
+ + + + + + + + + + +
+ ); +} + +export default React.memo(UpdateWebhookPage); diff --git a/cvat-ui/src/components/webhooks-page/empty-list.tsx b/cvat-ui/src/components/webhooks-page/empty-list.tsx new file mode 100644 index 000000000000..a1ffbd1dfe10 --- /dev/null +++ b/cvat-ui/src/components/webhooks-page/empty-list.tsx @@ -0,0 +1,35 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import Text from 'antd/lib/typography/Text'; +import { Row, Col } from 'antd/lib/grid'; + +import Empty from 'antd/lib/empty'; +import { WebhooksQuery } from 'reducers'; + +interface Props { + query: WebhooksQuery; +} + +function EmptyWebhooksListComponent(props: Props): JSX.Element { + const { query } = props; + + return ( +
+ + + + No webhooks created yet ... + + + + ) : (No results matched your search)} + /> +
+ ); +} + +export default React.memo(EmptyWebhooksListComponent); diff --git a/cvat-ui/src/components/webhooks-page/styles.scss b/cvat-ui/src/components/webhooks-page/styles.scss new file mode 100644 index 000000000000..6b028f53c59e --- /dev/null +++ b/cvat-ui/src/components/webhooks-page/styles.scss @@ -0,0 +1,136 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../styles.scss'; + +.cvat-webhooks-list { + height: 100%; + overflow-y: auto; + margin-top: $grid-unit-size * 3; +} + +.cvat-webhooks-list-item { + width: 100%; + height: $grid-unit-size * 16; + border: 1px solid $border-color-1; + border-radius: 3px; + margin-bottom: $grid-unit-size * 2; + padding: $grid-unit-size * 2 0 $grid-unit-size * 0.5 0; + background: $background-color-1; + + @media screen and (min-width: 1080px) { + height: $grid-unit-size * 15; + } + + &:hover { + border: 1px solid $border-color-hover; + } + + .ant-typography-ellipsis { + margin-bottom: 0; + } +} + +.cvat-webhook-status { + margin-left: $grid-unit-size; +} + +.cvat-webhook-status-available { + @extend .cvat-webhook-status; + + color: green; + fill: green; +} + +.cvat-webhook-status-failed { + @extend .cvat-webhook-status; + + color: red; + fill: red; +} + +.cvat-webhook-status-unavailable { + @extend .cvat-webhook-status; + + color: gray; + fill: gray; +} + +.cvat-webhook-info-text { + margin-right: $grid-unit-size; +} + +.cvat-item-ping-webhook-button { + margin-right: $grid-unit-size * 3; +} + +.cvat-webhooks-page-actions-button { + margin-right: $grid-unit-size; + margin-top: $grid-unit-size; + display: flex; + align-items: center; + padding: $grid-unit-size; + line-height: $grid-unit-size * 2; +} + +.cvat-webhooks-page { + width: 100%; + height: 100%; + padding-top: $grid-unit-size * 3; + + > div:nth-child(1) { + padding-bottom: $grid-unit-size; + } + + > div:nth-child(3) { + height: 83%; + margin-bottom: $grid-unit-size * 4; + } +} + +.cvat-empty-webhooks-list .ant-empty { + top: 50%; + left: 50%; + position: absolute; + transform: translate(-50%, -50%); +} + +.cvat-webhooks-page-top-bar { + > button { + margin-right: $grid-unit-size; + } + + > div { + display: flex; + justify-content: space-between; + } +} + +.cvat-webhooks-page-search-bar { + width: $grid-unit-size * 32; +} + +.cvat-webhooks-page-filters-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + + > div { + display: flex; + margin-right: $grid-unit-size * 4; + + > button { + margin-right: $grid-unit-size; + } + } +} + +.cvat-webhooks-add-wrapper { + display: inline-block; +} + +.cvat-webhooks-go-back { + padding: 0.5 * $grid-unit-size 0; +} diff --git a/cvat-ui/src/components/webhooks-page/top-bar.tsx b/cvat-ui/src/components/webhooks-page/top-bar.tsx new file mode 100644 index 000000000000..195054641cae --- /dev/null +++ b/cvat-ui/src/components/webhooks-page/top-bar.tsx @@ -0,0 +1,94 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useState } from 'react'; +import { Row, Col } from 'antd/lib/grid'; +import { PlusOutlined } from '@ant-design/icons'; +import Button from 'antd/lib/button'; +import Input from 'antd/lib/input'; + +import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering'; +import { WebhooksQuery } from 'reducers'; +import { + localStorageRecentKeyword, localStorageRecentCapacity, config, +} from './webhooks-filter-configuration'; + +const FilteringComponent = ResourceFilterHOC( + config, localStorageRecentKeyword, localStorageRecentCapacity, +); + +interface VisibleTopBarProps { + onApplyFilter(filter: string | null): void; + onApplySorting(sorting: string | null): void; + onApplySearch(search: string | null): void; + query: WebhooksQuery; + onCreateWebhook(): void; + goBackContent: JSX.Element; +} + +export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element { + const { + query, onApplyFilter, onApplySorting, onApplySearch, onCreateWebhook, goBackContent, + } = props; + const [visibility, setVisibility] = useState(defaultVisibility); + + return ( + <> + + + {goBackContent} + + + + +
+ { + onApplySearch(phrase); + }} + defaultValue={query.search || ''} + className='cvat-webhooks-page-search-bar' + placeholder='Search ...' + /> +
+ ( + setVisibility({ ...defaultVisibility, sorting: visible }) + )} + defaultFields={query.sort?.split(',') || ['-ID']} + sortingFields={['ID', 'Target URL', 'Owner', 'Description', 'Type', 'Updated date']} + onApplySorting={onApplySorting} + /> + ( + setVisibility({ ...defaultVisibility, predefined: visible }) + )} + onBuilderVisibleChange={(visible: boolean) => ( + setVisibility({ ...defaultVisibility, builder: visible }) + )} + onRecentVisibleChange={(visible: boolean) => ( + setVisibility({ + ...defaultVisibility, + builder: visibility.builder, + recent: visible, + }) + )} + onApplyFilter={onApplyFilter} + /> +
+
+
+
+ +
+ + ); +} diff --git a/cvat-ui/src/components/webhooks-page/webhook-item.tsx b/cvat-ui/src/components/webhooks-page/webhook-item.tsx new file mode 100644 index 000000000000..c57070c146b2 --- /dev/null +++ b/cvat-ui/src/components/webhooks-page/webhook-item.tsx @@ -0,0 +1,197 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useState } from 'react'; +import { useHistory } from 'react-router'; +import moment from 'moment'; +import { Col, Row } from 'antd/lib/grid'; +import Button from 'antd/lib/button'; +import Menu from 'antd/lib/menu'; +import Dropdown from 'antd/lib/dropdown'; +import Text from 'antd/lib/typography/Text'; +import { MoreOutlined } from '@ant-design/icons'; +import Modal from 'antd/lib/modal'; + +import { groupEvents } from 'components/setup-webhook-pages/setup-webhook-content'; +import Paragraph from 'antd/lib/typography/Paragraph'; +import CVATTooltip from 'components/common/cvat-tooltip'; +import { deleteWebhookAsync } from 'actions/webhooks-actions'; +import { useDispatch } from 'react-redux'; + +export interface WebhookItemProps { + webhookInstance: any; +} + +interface WebhookStatus { + message?: string; + className: string; +} + +function setUpWebhookStatus(status: number): WebhookStatus { + if (status && status.toString().startsWith('2')) { + return { + message: `Last delivery was succesful. Response: ${status}`, + className: 'cvat-webhook-status-available', + }; + } + if (status && status.toString().startsWith('5')) { + return { + message: `Last delivery was not succesful. Response: ${status}`, + className: 'cvat-webhook-status-failed', + }; + } + return { + message: status ? `Response: ${status}` : undefined, + className: 'cvat-webhook-status-unavailable', + }; +} + +function WebhookItem(props: WebhookItemProps): JSX.Element | null { + const [isRemoved, setIsRemoved] = useState(false); + const [pingFetching, setPingFetching] = useState(false); + const history = useHistory(); + const dispatch = useDispatch(); + const { webhookInstance } = props; + const { + id, description, updatedDate, createdDate, owner, targetURL, events, + } = webhookInstance; + + const updated = moment(updatedDate).fromNow(); + const created = moment(createdDate).format('MMMM Do YYYY'); + const username = owner ? owner.username : null; + + const { lastStatus } = webhookInstance; + const [webhookStatus, setWebhookStatus] = useState(setUpWebhookStatus(lastStatus)); + + const eventsList = groupEvents(events).join(', '); + return ( + + + { + webhookStatus.message ? ( + + + + + + ) : ( + + + + ) + } + + + + + {`#${id}: `} + {description} + + {username && ( + <> + {`Created by ${username} on ${created}`} +
+ + )} + {`Last updated ${updated}`} + + + + URL: + {targetURL} + + + + + Events: + {eventsList} + + + + + + + + + + + ( + + + { + e.preventDefault(); + history.push(`/webhooks/update/${id}`); + return false; + }} + > + Edit + + + { + Modal.confirm({ + title: 'Are you sure you want to remove the hook?', + content: 'It will stop notificating the specified URL about listed events', + className: 'cvat-modal-confirm-remove-webhook', + onOk: () => { + dispatch(deleteWebhookAsync(webhookInstance)).then(() => { + setIsRemoved(true); + }); + }, + }); + }} + > + Delete + + + )} + > +
+ Actions + +
+
+ +
+ +
+ ); +} + +export default React.memo(WebhookItem); diff --git a/cvat-ui/src/components/webhooks-page/webhooks-filter-configuration.ts b/cvat-ui/src/components/webhooks-page/webhooks-filter-configuration.ts new file mode 100644 index 000000000000..5593cb7bdc72 --- /dev/null +++ b/cvat-ui/src/components/webhooks-page/webhooks-filter-configuration.ts @@ -0,0 +1,55 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { Config } from 'react-awesome-query-builder'; + +export const config: Partial = { + fields: { + description: { + label: 'Description', + type: 'text', + valueSources: ['value'], + operators: ['like'], + }, + target_url: { + label: 'Target URL', + type: 'text', + valueSources: ['value'], + operators: ['like'], + }, + owner: { + label: 'Owner', + type: 'text', + valueSources: ['value'], + operators: ['equal'], + }, + updated_date: { + label: 'Last updated', + type: 'datetime', + operators: ['between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'], + }, + type: { + label: 'Type', + type: 'select', + valueSources: ['value'], + fieldSettings: { + listValues: [ + { value: 'organization', title: 'Organization' }, + { value: 'project', title: 'Project' }, + ], + }, + }, + id: { + label: 'ID', + type: 'number', + operators: ['equal', 'between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'], + fieldSettings: { min: 0 }, + valueSources: ['value'], + }, + }, +}; + +export const localStorageRecentCapacity = 10; +export const localStorageRecentKeyword = 'recentlyAppliedWebhooksFilters'; +export const predefinedFilterValues = {}; diff --git a/cvat-ui/src/components/webhooks-page/webhooks-list.tsx b/cvat-ui/src/components/webhooks-page/webhooks-list.tsx new file mode 100644 index 000000000000..ac9bead1abc6 --- /dev/null +++ b/cvat-ui/src/components/webhooks-page/webhooks-list.tsx @@ -0,0 +1,29 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { Row, Col } from 'antd/lib/grid'; +import { useSelector } from 'react-redux'; +import { CombinedState } from 'reducers'; +import WebhookItem from './webhook-item'; + +function WebhooksList(): JSX.Element { + const webhooks = useSelector((state: CombinedState) => state.webhooks.current); + return ( + + + {webhooks.map( + (webhook: any): JSX.Element => ( + + ), + )} + + + ); +} + +export default React.memo(WebhooksList); diff --git a/cvat-ui/src/components/webhooks-page/webhooks-page.tsx b/cvat-ui/src/components/webhooks-page/webhooks-page.tsx new file mode 100644 index 000000000000..058ac52b6b97 --- /dev/null +++ b/cvat-ui/src/components/webhooks-page/webhooks-page.tsx @@ -0,0 +1,151 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + useHistory, useRouteMatch, +} from 'react-router'; +import Spin from 'antd/lib/spin'; +import { Row, Col } from 'antd/lib/grid'; +import Pagination from 'antd/lib/pagination'; +import Button from 'antd/lib/button'; + +import { CombinedState, Indexable } from 'reducers'; +import { updateHistoryFromQuery } from 'components/resource-sorting-filtering'; +import { getWebhooksAsync } from 'actions/webhooks-actions'; +import { LeftOutlined } from '@ant-design/icons'; +import WebhooksList from './webhooks-list'; +import TopBar from './top-bar'; +import EmptyWebhooksListComponent from './empty-list'; + +interface ProjectRouteMatch { + id?: string | undefined; +} + +const PAGE_SIZE = 10; + +function WebhooksPage(): JSX.Element | null { + const dispatch = useDispatch(); + const history = useHistory(); + const organization = useSelector((state: CombinedState) => state.organizations.current); + const fetching = useSelector((state: CombinedState) => state.webhooks.fetching); + const totalCount = useSelector((state: CombinedState) => state.webhooks.totalCount); + const query = useSelector((state: CombinedState) => state.webhooks.query); + + const projectsMatch = useRouteMatch({ path: '/projects/:id/webhooks' }); + + const [onCreateParams, setOnCreateParams] = useState(null); + const onCreateWebhook = useCallback(() => { + history.push(`/webhooks/create?${onCreateParams || ''}`); + }, [onCreateParams]); + + const goBackContent = ( + + ); + + const queryParams = new URLSearchParams(history.location.search); + const updatedQuery = { ...query }; + for (const key of Object.keys(updatedQuery)) { + (updatedQuery as Indexable)[key] = queryParams.get(key) || null; + if (key === 'page') { + updatedQuery.page = updatedQuery.page ? +updatedQuery.page : 1; + } + } + + useEffect(() => { + if (projectsMatch) { + const { id } = projectsMatch.params; + setOnCreateParams(`projectId=${id}`); + dispatch(getWebhooksAsync({ ...updatedQuery, projectId: +id })); + } else if (organization) { + dispatch(getWebhooksAsync(updatedQuery)); + } else { + history.push('/'); + } + }, [organization]); + + useEffect(() => { + history.replace({ + search: updateHistoryFromQuery(query), + }); + }, [query]); + + const content = totalCount ? ( + <> + + + + { + dispatch(getWebhooksAsync({ + ...query, + page, + })); + }} + showSizeChanger={false} + total={totalCount} + current={query.page} + pageSize={PAGE_SIZE} + showQuickJumper + /> + + + + ) : ; + + return ( +
+ { + dispatch( + getWebhooksAsync({ + ...query, + search, + page: 1, + }), + ); + }} + onApplyFilter={(filter: string | null) => { + dispatch( + getWebhooksAsync({ + ...query, + filter, + page: 1, + }), + ); + }} + onApplySorting={(sorting: string | null) => { + dispatch( + getWebhooksAsync({ + ...query, + sort: sorting, + page: 1, + }), + ); + }} + /> + { fetching ? ( +
+ +
+ ) : content } +
+ ); +} + +export default React.memo(WebhooksPage); diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index 888bf20a7f52..b136123ed67b 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -4,6 +4,7 @@ import _cvat from 'cvat-core/src/api'; import ObjectState from 'cvat-core/src/object-state'; +import Webhook from 'cvat-core/src/webhook'; import { Label, Attribute, RawAttribute, RawLabel, } from 'cvat-core/src/labels'; @@ -28,6 +29,7 @@ export { Attribute, ShapeType, Storage, + Webhook, }; export type { diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index ed09a69c2722..29af97ae5565 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -6,6 +6,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { Canvas3d } from 'cvat-canvas3d/src/typescript/canvas3d'; import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper'; +import { Webhook } from 'cvat-core-wrapper'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { KeyMap } from 'utils/mousetrap-react'; import { OpenCVTracker } from 'utils/opencv-wrapper/opencv-interfaces'; @@ -510,6 +511,12 @@ export interface NotificationsState { updatingMembership: null | ErrorState; removingMembership: null | ErrorState; }; + webhooks: { + fetching: null | ErrorState; + creating: null | ErrorState; + updating: null | ErrorState; + deleting: null | ErrorState; + }; }; messages: { tasks: { @@ -837,6 +844,22 @@ export interface OrganizationState { updatingMember: boolean; } +export interface WebhooksQuery { + page: number; + id: number | null; + search: string | null; + filter: string | null; + sort: string | null; + projectId: number | null; +} + +export interface WebhooksState { + current: Webhook[], + totalCount: number; + fetching: boolean; + query: WebhooksQuery; +} + export interface CombinedState { auth: AuthState; projects: ProjectsState; @@ -857,6 +880,7 @@ export interface CombinedState { import: ImportState; cloudStorages: CloudStoragesState; organizations: OrganizationState; + webhooks: WebhooksState; } export enum DimensionType { diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 0bb87a83b49e..c81778b00554 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -22,6 +22,7 @@ import { ImportActionTypes } from 'actions/import-actions'; import { CloudStorageActionTypes } from 'actions/cloud-storage-actions'; import { OrganizationActionsTypes } from 'actions/organization-actions'; import { JobsActionTypes } from 'actions/jobs-actions'; +import { WebhooksActionsTypes } from 'actions/webhooks-actions'; import { NotificationsState } from '.'; @@ -150,6 +151,12 @@ const defaultState: NotificationsState = { updatingMembership: null, removingMembership: null, }, + webhooks: { + fetching: null, + creating: null, + updating: null, + deleting: null, + }, }, messages: { tasks: { @@ -1621,6 +1628,70 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case WebhooksActionsTypes.GET_WEBHOOKS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + webhooks: { + ...state.errors.webhooks, + fetching: { + message: 'Could not fetch a list of webhooks', + reason: action.payload.error.toString(), + className: 'cvat-notification-notice-get-webhooks-failed', + }, + }, + }, + }; + } + case WebhooksActionsTypes.CREATE_WEBHOOK_FAILED: { + return { + ...state, + errors: { + ...state.errors, + webhooks: { + ...state.errors.webhooks, + creating: { + message: 'Could not create webhook', + reason: action.payload.error.toString(), + className: 'cvat-notification-notice-create-webhook-failed', + }, + }, + }, + }; + } + case WebhooksActionsTypes.UPDATE_WEBHOOK_FAILED: { + return { + ...state, + errors: { + ...state.errors, + webhooks: { + ...state.errors.webhooks, + updating: { + message: 'Could not update webhook', + reason: action.payload.error.toString(), + className: 'cvat-notification-notice-update-webhook-failed', + }, + }, + }, + }; + } + case WebhooksActionsTypes.DELETE_WEBHOOK_FAILED: { + return { + ...state, + errors: { + ...state.errors, + webhooks: { + ...state.errors.webhooks, + deleting: { + message: 'Could not delete webhook', + reason: action.payload.error.toString(), + className: 'cvat-notification-notice-delete-webhook-failed', + }, + }, + }, + }; + } case BoundariesActionTypes.RESET_AFTER_ERROR: case AuthActionTypes.LOGOUT_SUCCESS: { return { ...defaultState }; diff --git a/cvat-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index a55c4bc48658..1371015bbce4 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -23,6 +23,7 @@ import exportReducer from './export-reducer'; import importReducer from './import-reducer'; import cloudStoragesReducer from './cloud-storages-reducer'; import organizationsReducer from './organizations-reducer'; +import webhooksReducer from './webhooks-reducer'; export default function createRootReducer(): Reducer { return combineReducers({ @@ -45,5 +46,6 @@ export default function createRootReducer(): Reducer { import: importReducer, cloudStorages: cloudStoragesReducer, organizations: organizationsReducer, + webhooks: webhooksReducer, }); } diff --git a/cvat-ui/src/reducers/webhooks-reducer.ts b/cvat-ui/src/reducers/webhooks-reducer.ts new file mode 100644 index 000000000000..6b201b3eadcf --- /dev/null +++ b/cvat-ui/src/reducers/webhooks-reducer.ts @@ -0,0 +1,56 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { AuthActions, AuthActionTypes } from 'actions/auth-actions'; +import { WebhooksActions, WebhooksActionsTypes } from 'actions/webhooks-actions'; +import { WebhooksState } from 'reducers'; + +const defaultState: WebhooksState = { + current: [], + totalCount: 0, + query: { + page: 1, + id: null, + projectId: null, + search: null, + filter: null, + sort: null, + }, + fetching: false, +}; + +export default function ( + state: WebhooksState = defaultState, + action: WebhooksActions | AuthActions, +): WebhooksState { + switch (action.type) { + case WebhooksActionsTypes.GET_WEBHOOKS: { + return { + ...state, + fetching: true, + query: { + ...state.query, + ...action.payload.query, + }, + }; + } + case WebhooksActionsTypes.GET_WEBHOOKS_SUCCESS: + return { + ...state, + fetching: false, + totalCount: action.payload.count, + current: action.payload.webhooks, + }; + case WebhooksActionsTypes.GET_WEBHOOKS_FAILED: + return { + ...state, + fetching: false, + }; + case AuthActionTypes.LOGOUT_SUCCESS: { + return { ...defaultState }; + } + default: + return state; + } +} diff --git a/cvat/apps/engine/mixins.py b/cvat/apps/engine/mixins.py index d4ec577de4e0..8b080c081f30 100644 --- a/cvat/apps/engine/mixins.py +++ b/cvat/apps/engine/mixins.py @@ -1,4 +1,5 @@ # Copyright (C) 2021-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -15,6 +16,7 @@ from cvat.apps.engine.models import Location from cvat.apps.engine.location import StorageType, get_location_configuration from cvat.apps.engine.serializers import DataSerializer, LabeledDataSerializer +from cvat.apps.webhooks.signals import signal_update, signal_create, signal_delete class TusFile: _tus_cache_timeout = 3600 @@ -321,6 +323,12 @@ def deserialize(self, request, import_func): return import_func(request, filename=file_name) return self.upload_data(request) + +class CreateModelMixin(mixins.CreateModelMixin): + def perform_create(self, serializer): + super().perform_create(serializer) + signal_create.send(self, instance=serializer.instance) + class PartialUpdateModelMixin: """ Update fields of a model instance. @@ -329,8 +337,23 @@ class PartialUpdateModelMixin: """ def perform_update(self, serializer): + old_values = { + attr: serializer.to_representation(serializer.instance).get(attr, None) + for attr in self.request.data.keys() + } + mixins.UpdateModelMixin.perform_update(self, serializer=serializer) + if getattr(serializer.instance, '_prefetched_objects_cache', None): + serializer.instance._prefetched_objects_cache = {} + + signal_update.send(self, instance=serializer.instance, old_values=old_values) + def partial_update(self, request, *args, **kwargs): kwargs['partial'] = True return mixins.UpdateModelMixin.update(self, request=request, *args, **kwargs) + +class DestroyModelMixin(mixins.DestroyModelMixin): + def perform_destroy(self, instance): + signal_delete.send(self, instance=instance) + super().perform_destroy(instance) diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 43f14dc46ccf..93798fe793a5 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -468,6 +468,9 @@ def get_project_id(self): project = self.segment.task.project return project.id if project else None + def get_organization_id(self): + return self.segment.task.organization + def get_bug_tracker(self): task = self.segment.task project = task.project @@ -675,6 +678,12 @@ class Issue(models.Model): updated_date = models.DateTimeField(null=True, blank=True) resolved = models.BooleanField(default=False) + def get_project_id(self): + return self.job.get_project_id() + + def get_organization_id(self): + return self.job.get_organization_id() + class Comment(models.Model): issue = models.ForeignKey(Issue, related_name='comments', on_delete=models.CASCADE) owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) @@ -682,6 +691,12 @@ class Comment(models.Model): created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) + def get_project_id(self): + return self.issue.get_project_id() + + def get_organization_id(self): + return self.issue.get_organization_id() + class CloudProviderChoice(str, Enum): AWS_S3 = 'AWS_S3_BUCKET' AZURE_CONTAINER = 'AZURE_CONTAINER' diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 423ca6ed6422..37c2a28e1c92 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -41,6 +41,7 @@ from rest_framework.exceptions import PermissionDenied from django_sendfile import sendfile +from cvat.apps.webhooks.signals import signal_create, signal_update import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager.views # pylint: disable=unused-import from cvat.apps.engine.cloud_provider import ( @@ -72,7 +73,7 @@ from utils.dataset_manifest import ImageManifestManager from cvat.apps.engine.utils import av_scan_paths, process_failed_job, configure_dependent_job from cvat.apps.engine import backup -from cvat.apps.engine.mixins import PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin +from cvat.apps.engine.mixins import PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin, DestroyModelMixin from cvat.apps.engine.location import get_location_configuration, StorageType from . import models, task @@ -270,7 +271,7 @@ def plugins(request): }) ) class ProjectViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, - mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, + mixins.RetrieveModelMixin, mixins.CreateModelMixin, DestroyModelMixin, PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin ): queryset = models.Project.objects.prefetch_related(Prefetch('label_set', @@ -307,6 +308,7 @@ def get_queryset(self): def perform_create(self, serializer): serializer.save(owner=self.request.user, organization=self.request.iam_context['organization']) + signal_create.send(self, instance=serializer.instance) @extend_schema( summary='Method returns information of the tasks of the project with the selected id', @@ -710,7 +712,7 @@ def __call__(self, request, start, stop, db_data, db_object): }) ) class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, - mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, + mixins.RetrieveModelMixin, mixins.CreateModelMixin, DestroyModelMixin, PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin ): queryset = Task.objects.prefetch_related( @@ -798,12 +800,25 @@ def export_backup(self, request, pk=None): def perform_update(self, serializer): instance = serializer.instance + + old_values = {} + old_repr = serializer.to_representation(instance) + for attr in self.request.data.keys(): + old_values[attr] = old_repr[attr] if attr in old_repr \ + else getattr(instance, attr, None) + updated_instance = serializer.save() + if instance.project: instance.project.save() if updated_instance.project: updated_instance.project.save() + if getattr(instance, '_prefetched_objects_cache', None): + instance._prefetched_objects_cache = {} + + signal_update.send(self, instance=serializer.instance, old_values=old_values) + def perform_create(self, serializer): instance = serializer.save(owner=self.request.user, organization=self.request.iam_context['organization']) @@ -811,6 +826,7 @@ def perform_create(self, serializer): db_project = instance.project db_project.save() assert instance.organization == db_project.organization + signal_create.send(self, instance=serializer.instance) def perform_destroy(self, instance): task_dirname = instance.get_dirname() @@ -823,6 +839,7 @@ def perform_destroy(self, instance): db_project = instance.project db_project.save() + @extend_schema(summary='Method returns a list of jobs for a specific task', responses=JobReadSerializer(many=True)) # Duplicate to still get 'list' op. name @action(detail=True, methods=['GET'], serializer_class=JobReadSerializer(many=True), @@ -1472,6 +1489,7 @@ def annotations(self, request, pk): return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST) return Response(data) + @extend_schema(methods=['PATCH'], operation_id='jobs_partial_update_annotations_file', summary="Allows to upload an annotation file chunk. " @@ -1487,6 +1505,7 @@ def append_annotations_chunk(self, request, pk, file_id): self._object = self.get_object() return self.append_tus_chunk(request, file_id) + @extend_schema(summary='Export job as a dataset in a specific format', parameters=[ OpenApiParameter('format', location=OpenApiParameter.QUERY, @@ -1539,6 +1558,7 @@ def issues(self, request, pk): return Response(serializer.data) + @extend_schema(summary='Method returns data for a specific job', parameters=[ OpenApiParameter('type', description='Specifies the type of the requested data', @@ -1566,6 +1586,7 @@ def data(self, request, pk): return data_getter(request, db_job.segment.start_frame, db_job.segment.stop_frame, db_job.segment.task.data, db_job) + @extend_schema(summary='Method provides a meta information about media files which are related with the job', responses={ '200': DataMetaReadSerializer, @@ -1685,7 +1706,7 @@ def commits(self, request, pk): }) ) class IssueViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, - mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, + mixins.RetrieveModelMixin, mixins.CreateModelMixin, DestroyModelMixin, PartialUpdateModelMixin ): queryset = Issue.objects.all().order_by('-id') @@ -1717,6 +1738,7 @@ def get_serializer_class(self): def perform_create(self, serializer): serializer.save(owner=self.request.user) + signal_create.send(self, instance=serializer.instance) @extend_schema(summary='The action returns all comments of a specific issue', responses=CommentReadSerializer(many=True)) # Duplicate to still get 'list' op. name @@ -1765,7 +1787,7 @@ def comments(self, request, pk): }) ) class CommentViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, - mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, + mixins.RetrieveModelMixin, mixins.CreateModelMixin, DestroyModelMixin, PartialUpdateModelMixin ): queryset = Comment.objects.all().order_by('-id') @@ -1792,6 +1814,7 @@ def get_serializer_class(self): def perform_create(self, serializer): serializer.save(owner=self.request.user) + signal_create.send(self, instance=serializer.instance) @extend_schema(tags=['users']) @extend_schema_view( diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index fe5a6d152a39..4774ea378b59 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -1,4 +1,5 @@ # Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -12,6 +13,7 @@ from django.db.models import Q from rest_framework.permissions import BasePermission +from cvat.apps.webhooks.models import Webhook from cvat.apps.organizations.models import Membership, Organization from cvat.apps.engine.models import Project, Task, Job, Issue @@ -763,6 +765,99 @@ def get_resource(self): return data + +class WebhookPermission(OpenPolicyAgentPermission): + @classmethod + def create(cls, request, view, obj): + 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, + 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) + permissions.append(perm) + + if project_id: + perm = ProjectPermission.create_scope_view(request, project_id) + permissions.append(perm) + + return permissions + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.url = settings.IAM_OPA_DATA_URL + '/webhooks/allow' + + @staticmethod + def get_scopes(request, view, obj): + scope = { + ('create', 'POST'): 'create', + ('destroy', 'DELETE'): 'delete', + ('partial_update', 'PATCH'): 'update', + ('update', 'PUT'): 'update', + ('list', 'GET'): 'list', + ('retrieve', 'GET'): 'view', + }.get((view.action, request.method)) + + scopes = [] + if scope == 'create': + webhook_type = request.data.get('type') + if webhook_type: + scope += f'@{webhook_type}' + scopes.append(scope) + elif scope in ['update', 'delete', 'list', 'view']: + scopes.append(scope) + + return scopes + + def get_resource(self): + data = None + if self.obj: + data = { + "id": self.obj.id, + "owner": {"id": getattr(self.obj.owner, 'id', None) }, + 'organization': { + "id": getattr(self.obj.organization, 'id', None) + }, + "project": None + } + if self.obj.type == 'project' and getattr(self.obj, 'project', None): + data['project'] = { + 'owner': {'id': getattr(self.obj.project.owner, 'id', None)} + } + elif self.scope in ['create@project', 'create@organization']: + project = None + if self.project_id: + try: + project = Project.objects.get(id=self.project_id) + except Project.DoesNotExist: + raise ValidationError(f"Could not find project with provided id: {self.project_id}") + + num_resources = Webhook.objects.filter(project=self.project_id).count() if project \ + else Webhook.objects.filter(organization=self.org_id, project=None).count() + + data = { + 'id': None, + 'owner': self.user_id, + 'organization': { + 'id': self.org_id + }, + 'num_resources': num_resources + } + + data['project'] = None if project is None else { + 'owner': { + 'id': getattr(project.owner, 'id', None) + }, + } + + return data + class JobPermission(OpenPolicyAgentPermission): @classmethod def create(cls, request, view, obj): @@ -1029,6 +1124,7 @@ def get_common_data(db_job): return data + class PolicyEnforcer(BasePermission): # pylint: disable=no-self-use def check_permission(self, request, view, obj): @@ -1071,3 +1167,4 @@ def has_permission(self, request, view): return membership is not None return True + diff --git a/cvat/apps/iam/rules/utils.rego b/cvat/apps/iam/rules/utils.rego index 430427f78c2a..99025da1f244 100644 --- a/cvat/apps/iam/rules/utils.rego +++ b/cvat/apps/iam/rules/utils.rego @@ -35,6 +35,7 @@ UPDATE_OWNER := "update:owner" EXPORT_ANNOTATIONS := "export:annotations" EXPORT_DATASET := "export:dataset" CREATE_IN_PROJECT := "create@project" +CREATE_IN_ORGANIZATION := "create@organization" UPDATE_PROJECT := "update:project" VIEW_ANNOTATIONS := "view:annotations" UPDATE_ANNOTATIONS := "update:annotations" diff --git a/cvat/apps/iam/rules/webhooks.csv b/cvat/apps/iam/rules/webhooks.csv new file mode 100644 index 000000000000..eb87a418b916 --- /dev/null +++ b/cvat/apps/iam/rules/webhooks.csv @@ -0,0 +1,21 @@ +Scope,Resource,Context,Ownership,Limit,Method,URL,Privilege,Membership +create@project,Webhook,Sandbox,N/A,,POST,/webhooks,Admin,N/A +create@project,Webhook,Sandbox,Project:owner,resource['num_resources'] < 10,POST,/webhooks,Worker,N/A +create@project,Webhook,Organization,N/A,resource['num_resources'] < 10,POST,/webhooks,Worker,Maintainer +create@project,Webhook,Organization,Project:owner,resource['num_resources'] < 10,POST,/webhooks,Worker,Worker +create@organization,Webhook,Organization,N/A,,POST,/webhooks,Admin,N/A +create@organization,Webhook,Organization,N/A,resource['num_resources'] < 10,POST,/webhooks,Worker,Maintainer +update,Webhook,Sandbox,N/A,,PATCH,/webhooks/{id},Admin,N/A +update,Webhook,Sandbox,"Project:owner, owner",,PATCH,/webhooks/{id},Worker,N/A +update,Webhook,Organization,N/A,,PATCH,/webhooks/{id},Worker,Maintainer +update,Webhook,Organization,"Project:owner, owner",,PATCH,/webhooks/{id},Worker,Worker +delete,Webhook,Sandbox,N/A,,DELETE,/webhooks/{id},Admin,N/A +delete,Webhook,Sandbox,"Project:owner, owner",,DELETE,/webhooks/{id},Worker,N/A +delete,Webhook,Organization,N/A,,DELETE,/webhooks/{id},Worker,Maintainer +delete,Webhook,Organization,"Project:owner, owner",,DELETE,/webhooks/{id},Worker,Worker +view,Webhook,Sandbox,N/A,,GET,/webhooks/{id},Admin,N/A +view,Webhook,Sandbox,"Project:owner, owner",,GET,/webhooks/{id},None,N/A +view,Webhook,Organization,N/A,,GET,/webhooks/{id},Worker,Maintainer +view,Webhook,Organization,"Project:owner, owner",,GET,/webhooks/{id},None,Worker +list,N/A,Sandbox,N/A,,GET,/webhooks,None,N/A +list,N/A,Organization,N/A,,GET,/webhooks,None,Worker diff --git a/cvat/apps/iam/rules/webhooks.rego b/cvat/apps/iam/rules/webhooks.rego new file mode 100644 index 000000000000..bf28ecaf9407 --- /dev/null +++ b/cvat/apps/iam/rules/webhooks.rego @@ -0,0 +1,173 @@ +package webhooks +import data.utils +import data.organizations + +# input : { +# "scope": <"create@project" | "create@organization" | "update" | "delete" | +# "list" | "view"> or null, +# "auth": { +# "user": { +# "id": +# "privilege": <"admin"|"business"|"user"|"worker"> or null +# } +# "organization": { +# "id": , +# "owner": +# "id": +# }, +# "user": { +# "role": <"owner"|"maintainer"|"supervisor"|"worker"> or null +# } +# } or null, +# }, +# "resource": { +# "id": , +# "owner": { "id": }, +# "organization": { "id": } or null, +# "project": { +# "owner": { "id": num }, +# } or null, +# "num_resources": +# } +# } +# + +is_project_owner { + input.resource.project.owner.id == input.auth.user.id +} + +is_webhook_owner { + input.resource.owner.id == input.auth.user.id +} + +default allow = false + +allow { + utils.is_admin +} + +allow { + input.scope == utils.CREATE_IN_PROJECT + utils.is_sandbox + utils.has_perm(utils.USER) + is_project_owner + input.resource.num_resources < 10 +} + + +allow { + input.scope == utils.LIST + utils.is_sandbox +} + +allow { + input.scope == utils.LIST + organizations.is_member +} + +filter = [] { # Django Q object to filter list of entries + utils.is_admin + utils.is_sandbox +} else = qobject { + utils.is_admin + utils.is_organization + qobject := [ {"organization": input.auth.organization.id} ] +} else = qobject { + utils.is_sandbox + user := input.auth.user + qobject := [ {"owner_id": user.id}, {"project__owner_id": user.id}, "|" ] +} else = qobject { + utils.is_organization + utils.has_perm(utils.WORKER) + organizations.has_perm(organizations.MAINTAINER) + qobject := [ {"organization": input.auth.organization.id} ] +} else = qobject { + utils.is_organization + utils.has_perm(utils.WORKER) + organizations.has_perm(organizations.WORKER) + user := input.auth.user + qobject := [ {"owner_id": user.id}, {"project__owner_id": user.id}, + "|", {"organization": input.auth.organization.id}, "&"] +} + + +allow { + input.scope == utils.VIEW + utils.is_sandbox + utils.is_resource_owner +} + +allow { + input.scope == utils.VIEW + utils.is_sandbox + is_project_owner +} + +allow { + { utils.UPDATE, utils.DELETE }[input.scope] + utils.is_sandbox + utils.has_perm(utils.WORKER) + utils.is_resource_owner +} + +allow { + { utils.UPDATE, utils.DELETE }[input.scope] + utils.is_sandbox + utils.has_perm(utils.WORKER) + is_project_owner +} + +allow { + input.scope == utils.VIEW + input.auth.organization.id == input.resource.organization.id + organizations.has_perm(organizations.WORKER) + utils.is_resource_owner +} + +allow { + input.scope == utils.VIEW + input.auth.organization.id == input.resource.organization.id + organizations.has_perm(organizations.WORKER) + is_project_owner +} + +allow { + { utils.UPDATE, utils.DELETE }[input.scope] + input.auth.organization.id == input.resource.organization.id + utils.has_perm(utils.WORKER) + organizations.has_perm(organizations.WORKER) + utils.is_resource_owner +} + + +allow { + { utils.UPDATE, utils.DELETE, utils.VIEW }[input.scope] + input.auth.organization.id == input.resource.organization.id + utils.has_perm(utils.WORKER) + organizations.has_perm(organizations.MAINTAINER) +} + +allow { + { utils.CREATE_IN_PROJECT, utils.CREATE_IN_ORGANIZATION }[input.scope] + input.auth.organization.id == input.resource.organization.id + utils.has_perm(utils.WORKER) + organizations.has_perm(organizations.MAINTAINER) + input.resource.num_resources < 10 +} + +allow { + { utils.UPDATE, utils.DELETE }[input.scope] + input.auth.organization.id == input.resource.organization.id + utils.has_perm(utils.WORKER) + organizations.has_perm(organizations.WORKER) + is_project_owner +} + +allow { + { utils.CREATE_IN_PROJECT }[input.scope] + input.auth.organization.id == input.resource.organization.id + utils.has_perm(utils.WORKER) + organizations.has_perm(organizations.WORKER) + input.resource.num_resources < 10 + is_project_owner +} diff --git a/cvat/apps/iam/rules/webhooks_test.gen.rego b/cvat/apps/iam/rules/webhooks_test.gen.rego new file mode 100644 index 000000000000..1eabc9a1fd71 --- /dev/null +++ b/cvat/apps/iam/rules/webhooks_test.gen.rego @@ -0,0 +1,2206 @@ +package webhooks + +test_scope_VIEW_context_SANDBOX_ownership_PROJECT_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 97, "privilege": "admin"}, "organization": null}, "resource": {"id": 157, "owner": {"id": 275}, "organization": {"id": 335}, "project": {"owner": {"id": 97}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 12, "privilege": "business"}, "organization": null}, "resource": {"id": 198, "owner": {"id": 237}, "organization": {"id": 310}, "project": {"owner": {"id": 12}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_PROJECT_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 27, "privilege": "user"}, "organization": null}, "resource": {"id": 140, "owner": {"id": 251}, "organization": {"id": 334}, "project": {"owner": {"id": 27}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_PROJECT_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 76, "privilege": "worker"}, "organization": null}, "resource": {"id": 176, "owner": {"id": 208}, "organization": {"id": 349}, "project": {"owner": {"id": 76}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_PROJECT_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 99, "privilege": "none"}, "organization": null}, "resource": {"id": 147, "owner": {"id": 297}, "organization": {"id": 320}, "project": {"owner": {"id": 99}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 85, "privilege": "admin"}, "organization": null}, "resource": {"id": 125, "owner": {"id": 85}, "organization": {"id": 339}, "project": {"owner": {"id": 451}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 52, "privilege": "business"}, "organization": null}, "resource": {"id": 173, "owner": {"id": 52}, "organization": {"id": 331}, "project": {"owner": {"id": 460}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 96, "privilege": "user"}, "organization": null}, "resource": {"id": 183, "owner": {"id": 96}, "organization": {"id": 301}, "project": {"owner": {"id": 411}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 7, "privilege": "worker"}, "organization": null}, "resource": {"id": 161, "owner": {"id": 7}, "organization": {"id": 367}, "project": {"owner": {"id": 420}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 58, "privilege": "none"}, "organization": null}, "resource": {"id": 140, "owner": {"id": 58}, "organization": {"id": 309}, "project": {"owner": {"id": 401}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 43, "privilege": "admin"}, "organization": null}, "resource": {"id": 136, "owner": {"id": 277}, "organization": {"id": 326}, "project": {"owner": {"id": 491}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 39, "privilege": "business"}, "organization": null}, "resource": {"id": 118, "owner": {"id": 255}, "organization": {"id": 316}, "project": {"owner": {"id": 405}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 28, "privilege": "user"}, "organization": null}, "resource": {"id": 158, "owner": {"id": 244}, "organization": {"id": 339}, "project": {"owner": {"id": 429}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 0, "privilege": "worker"}, "organization": null}, "resource": {"id": 132, "owner": {"id": 205}, "organization": {"id": 390}, "project": {"owner": {"id": 455}}}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 70, "privilege": "none"}, "organization": null}, "resource": {"id": 178, "owner": {"id": 272}, "organization": {"id": 338}, "project": {"owner": {"id": 451}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 41, "privilege": "admin"}, "organization": {"id": 159, "owner": {"id": 41}, "user": {"role": "owner"}}}, "resource": {"id": 155, "owner": {"id": 274}, "organization": {"id": 159}, "project": {"owner": {"id": 41}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 60, "privilege": "admin"}, "organization": {"id": 194, "owner": {"id": 60}, "user": {"role": "owner"}}}, "resource": {"id": 156, "owner": {"id": 286}, "organization": {"id": 327}, "project": {"owner": {"id": 60}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 84, "privilege": "admin"}, "organization": {"id": 181, "owner": {"id": 279}, "user": {"role": "maintainer"}}}, "resource": {"id": 184, "owner": {"id": 210}, "organization": {"id": 181}, "project": {"owner": {"id": 84}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 86, "privilege": "admin"}, "organization": {"id": 139, "owner": {"id": 228}, "user": {"role": "maintainer"}}}, "resource": {"id": 142, "owner": {"id": 211}, "organization": {"id": 396}, "project": {"owner": {"id": 86}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 31, "privilege": "admin"}, "organization": {"id": 160, "owner": {"id": 278}, "user": {"role": "supervisor"}}}, "resource": {"id": 125, "owner": {"id": 218}, "organization": {"id": 160}, "project": {"owner": {"id": 31}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 80, "privilege": "admin"}, "organization": {"id": 173, "owner": {"id": 224}, "user": {"role": "supervisor"}}}, "resource": {"id": 198, "owner": {"id": 209}, "organization": {"id": 358}, "project": {"owner": {"id": 80}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 51, "privilege": "admin"}, "organization": {"id": 131, "owner": {"id": 218}, "user": {"role": "worker"}}}, "resource": {"id": 191, "owner": {"id": 289}, "organization": {"id": 131}, "project": {"owner": {"id": 51}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 98, "privilege": "admin"}, "organization": {"id": 113, "owner": {"id": 299}, "user": {"role": "worker"}}}, "resource": {"id": 183, "owner": {"id": 288}, "organization": {"id": 300}, "project": {"owner": {"id": 98}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 66, "privilege": "admin"}, "organization": {"id": 159, "owner": {"id": 206}, "user": {"role": null}}}, "resource": {"id": 154, "owner": {"id": 228}, "organization": {"id": 159}, "project": {"owner": {"id": 66}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 17, "privilege": "admin"}, "organization": {"id": 159, "owner": {"id": 285}, "user": {"role": null}}}, "resource": {"id": 171, "owner": {"id": 231}, "organization": {"id": 315}, "project": {"owner": {"id": 17}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 96, "privilege": "business"}, "organization": {"id": 156, "owner": {"id": 96}, "user": {"role": "owner"}}}, "resource": {"id": 167, "owner": {"id": 271}, "organization": {"id": 156}, "project": {"owner": {"id": 96}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 57, "privilege": "business"}, "organization": {"id": 120, "owner": {"id": 57}, "user": {"role": "owner"}}}, "resource": {"id": 192, "owner": {"id": 264}, "organization": {"id": 354}, "project": {"owner": {"id": 57}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 31, "privilege": "business"}, "organization": {"id": 181, "owner": {"id": 235}, "user": {"role": "maintainer"}}}, "resource": {"id": 160, "owner": {"id": 257}, "organization": {"id": 181}, "project": {"owner": {"id": 31}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 80, "privilege": "business"}, "organization": {"id": 130, "owner": {"id": 235}, "user": {"role": "maintainer"}}}, "resource": {"id": 198, "owner": {"id": 299}, "organization": {"id": 366}, "project": {"owner": {"id": 80}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 30, "privilege": "business"}, "organization": {"id": 134, "owner": {"id": 242}, "user": {"role": "supervisor"}}}, "resource": {"id": 156, "owner": {"id": 209}, "organization": {"id": 134}, "project": {"owner": {"id": 30}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 19, "privilege": "business"}, "organization": {"id": 129, "owner": {"id": 249}, "user": {"role": "supervisor"}}}, "resource": {"id": 140, "owner": {"id": 269}, "organization": {"id": 310}, "project": {"owner": {"id": 19}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 8, "privilege": "business"}, "organization": {"id": 153, "owner": {"id": 252}, "user": {"role": "worker"}}}, "resource": {"id": 188, "owner": {"id": 219}, "organization": {"id": 153}, "project": {"owner": {"id": 8}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 7, "privilege": "business"}, "organization": {"id": 126, "owner": {"id": 253}, "user": {"role": "worker"}}}, "resource": {"id": 142, "owner": {"id": 269}, "organization": {"id": 359}, "project": {"owner": {"id": 7}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 2, "privilege": "business"}, "organization": {"id": 197, "owner": {"id": 273}, "user": {"role": null}}}, "resource": {"id": 149, "owner": {"id": 298}, "organization": {"id": 197}, "project": {"owner": {"id": 2}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 38, "privilege": "business"}, "organization": {"id": 196, "owner": {"id": 249}, "user": {"role": null}}}, "resource": {"id": 148, "owner": {"id": 261}, "organization": {"id": 300}, "project": {"owner": {"id": 38}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 69, "privilege": "user"}, "organization": {"id": 177, "owner": {"id": 69}, "user": {"role": "owner"}}}, "resource": {"id": 153, "owner": {"id": 268}, "organization": {"id": 177}, "project": {"owner": {"id": 69}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 62, "privilege": "user"}, "organization": {"id": 103, "owner": {"id": 62}, "user": {"role": "owner"}}}, "resource": {"id": 162, "owner": {"id": 228}, "organization": {"id": 334}, "project": {"owner": {"id": 62}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 92, "privilege": "user"}, "organization": {"id": 121, "owner": {"id": 259}, "user": {"role": "maintainer"}}}, "resource": {"id": 143, "owner": {"id": 285}, "organization": {"id": 121}, "project": {"owner": {"id": 92}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 50, "privilege": "user"}, "organization": {"id": 175, "owner": {"id": 272}, "user": {"role": "maintainer"}}}, "resource": {"id": 116, "owner": {"id": 279}, "organization": {"id": 368}, "project": {"owner": {"id": 50}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 54, "privilege": "user"}, "organization": {"id": 117, "owner": {"id": 259}, "user": {"role": "supervisor"}}}, "resource": {"id": 184, "owner": {"id": 203}, "organization": {"id": 117}, "project": {"owner": {"id": 54}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 41, "privilege": "user"}, "organization": {"id": 127, "owner": {"id": 258}, "user": {"role": "supervisor"}}}, "resource": {"id": 123, "owner": {"id": 206}, "organization": {"id": 333}, "project": {"owner": {"id": 41}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 35, "privilege": "user"}, "organization": {"id": 196, "owner": {"id": 253}, "user": {"role": "worker"}}}, "resource": {"id": 141, "owner": {"id": 243}, "organization": {"id": 196}, "project": {"owner": {"id": 35}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 95, "privilege": "user"}, "organization": {"id": 169, "owner": {"id": 206}, "user": {"role": "worker"}}}, "resource": {"id": 132, "owner": {"id": 210}, "organization": {"id": 360}, "project": {"owner": {"id": 95}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 99, "privilege": "user"}, "organization": {"id": 183, "owner": {"id": 205}, "user": {"role": null}}}, "resource": {"id": 144, "owner": {"id": 228}, "organization": {"id": 183}, "project": {"owner": {"id": 99}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 2, "privilege": "user"}, "organization": {"id": 179, "owner": {"id": 219}, "user": {"role": null}}}, "resource": {"id": 196, "owner": {"id": 203}, "organization": {"id": 331}, "project": {"owner": {"id": 2}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 14, "privilege": "worker"}, "organization": {"id": 172, "owner": {"id": 14}, "user": {"role": "owner"}}}, "resource": {"id": 130, "owner": {"id": 216}, "organization": {"id": 172}, "project": {"owner": {"id": 14}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 47, "privilege": "worker"}, "organization": {"id": 121, "owner": {"id": 47}, "user": {"role": "owner"}}}, "resource": {"id": 159, "owner": {"id": 289}, "organization": {"id": 332}, "project": {"owner": {"id": 47}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 99, "privilege": "worker"}, "organization": {"id": 120, "owner": {"id": 239}, "user": {"role": "maintainer"}}}, "resource": {"id": 177, "owner": {"id": 295}, "organization": {"id": 120}, "project": {"owner": {"id": 99}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 73, "privilege": "worker"}, "organization": {"id": 186, "owner": {"id": 248}, "user": {"role": "maintainer"}}}, "resource": {"id": 113, "owner": {"id": 274}, "organization": {"id": 303}, "project": {"owner": {"id": 73}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 75, "privilege": "worker"}, "organization": {"id": 188, "owner": {"id": 280}, "user": {"role": "supervisor"}}}, "resource": {"id": 150, "owner": {"id": 291}, "organization": {"id": 188}, "project": {"owner": {"id": 75}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 38, "privilege": "worker"}, "organization": {"id": 187, "owner": {"id": 276}, "user": {"role": "supervisor"}}}, "resource": {"id": 131, "owner": {"id": 213}, "organization": {"id": 389}, "project": {"owner": {"id": 38}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 68, "privilege": "worker"}, "organization": {"id": 154, "owner": {"id": 284}, "user": {"role": "worker"}}}, "resource": {"id": 115, "owner": {"id": 272}, "organization": {"id": 154}, "project": {"owner": {"id": 68}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 43, "privilege": "worker"}, "organization": {"id": 101, "owner": {"id": 253}, "user": {"role": "worker"}}}, "resource": {"id": 147, "owner": {"id": 208}, "organization": {"id": 364}, "project": {"owner": {"id": 43}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 81, "privilege": "worker"}, "organization": {"id": 158, "owner": {"id": 290}, "user": {"role": null}}}, "resource": {"id": 162, "owner": {"id": 213}, "organization": {"id": 158}, "project": {"owner": {"id": 81}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 66, "privilege": "worker"}, "organization": {"id": 183, "owner": {"id": 234}, "user": {"role": null}}}, "resource": {"id": 119, "owner": {"id": 255}, "organization": {"id": 322}, "project": {"owner": {"id": 66}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 59, "privilege": "none"}, "organization": {"id": 155, "owner": {"id": 59}, "user": {"role": "owner"}}}, "resource": {"id": 178, "owner": {"id": 268}, "organization": {"id": 155}, "project": {"owner": {"id": 59}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 11, "privilege": "none"}, "organization": {"id": 135, "owner": {"id": 11}, "user": {"role": "owner"}}}, "resource": {"id": 175, "owner": {"id": 234}, "organization": {"id": 341}, "project": {"owner": {"id": 11}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 78, "privilege": "none"}, "organization": {"id": 185, "owner": {"id": 248}, "user": {"role": "maintainer"}}}, "resource": {"id": 131, "owner": {"id": 296}, "organization": {"id": 185}, "project": {"owner": {"id": 78}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 23, "privilege": "none"}, "organization": {"id": 162, "owner": {"id": 227}, "user": {"role": "maintainer"}}}, "resource": {"id": 143, "owner": {"id": 203}, "organization": {"id": 363}, "project": {"owner": {"id": 23}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 76, "privilege": "none"}, "organization": {"id": 189, "owner": {"id": 235}, "user": {"role": "supervisor"}}}, "resource": {"id": 145, "owner": {"id": 233}, "organization": {"id": 189}, "project": {"owner": {"id": 76}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 10, "privilege": "none"}, "organization": {"id": 130, "owner": {"id": 292}, "user": {"role": "supervisor"}}}, "resource": {"id": 171, "owner": {"id": 201}, "organization": {"id": 366}, "project": {"owner": {"id": 10}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 30, "privilege": "none"}, "organization": {"id": 188, "owner": {"id": 260}, "user": {"role": "worker"}}}, "resource": {"id": 152, "owner": {"id": 262}, "organization": {"id": 188}, "project": {"owner": {"id": 30}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 2, "privilege": "none"}, "organization": {"id": 111, "owner": {"id": 237}, "user": {"role": "worker"}}}, "resource": {"id": 182, "owner": {"id": 291}, "organization": {"id": 362}, "project": {"owner": {"id": 2}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 39, "privilege": "none"}, "organization": {"id": 184, "owner": {"id": 274}, "user": {"role": null}}}, "resource": {"id": 128, "owner": {"id": 251}, "organization": {"id": 184}, "project": {"owner": {"id": 39}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 44, "privilege": "none"}, "organization": {"id": 154, "owner": {"id": 295}, "user": {"role": null}}}, "resource": {"id": 147, "owner": {"id": 260}, "organization": {"id": 370}, "project": {"owner": {"id": 44}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 58, "privilege": "admin"}, "organization": {"id": 134, "owner": {"id": 58}, "user": {"role": "owner"}}}, "resource": {"id": 170, "owner": {"id": 58}, "organization": {"id": 134}, "project": {"owner": {"id": 489}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 24, "privilege": "admin"}, "organization": {"id": 140, "owner": {"id": 24}, "user": {"role": "owner"}}}, "resource": {"id": 132, "owner": {"id": 24}, "organization": {"id": 315}, "project": {"owner": {"id": 492}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 23, "privilege": "admin"}, "organization": {"id": 124, "owner": {"id": 227}, "user": {"role": "maintainer"}}}, "resource": {"id": 195, "owner": {"id": 23}, "organization": {"id": 124}, "project": {"owner": {"id": 488}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 75, "privilege": "admin"}, "organization": {"id": 197, "owner": {"id": 267}, "user": {"role": "maintainer"}}}, "resource": {"id": 194, "owner": {"id": 75}, "organization": {"id": 335}, "project": {"owner": {"id": 492}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 37, "privilege": "admin"}, "organization": {"id": 129, "owner": {"id": 246}, "user": {"role": "supervisor"}}}, "resource": {"id": 176, "owner": {"id": 37}, "organization": {"id": 129}, "project": {"owner": {"id": 424}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 68, "privilege": "admin"}, "organization": {"id": 116, "owner": {"id": 235}, "user": {"role": "supervisor"}}}, "resource": {"id": 122, "owner": {"id": 68}, "organization": {"id": 301}, "project": {"owner": {"id": 490}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 89, "privilege": "admin"}, "organization": {"id": 116, "owner": {"id": 281}, "user": {"role": "worker"}}}, "resource": {"id": 105, "owner": {"id": 89}, "organization": {"id": 116}, "project": {"owner": {"id": 437}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 73, "privilege": "admin"}, "organization": {"id": 136, "owner": {"id": 260}, "user": {"role": "worker"}}}, "resource": {"id": 196, "owner": {"id": 73}, "organization": {"id": 313}, "project": {"owner": {"id": 401}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 6, "privilege": "admin"}, "organization": {"id": 132, "owner": {"id": 261}, "user": {"role": null}}}, "resource": {"id": 161, "owner": {"id": 6}, "organization": {"id": 132}, "project": {"owner": {"id": 423}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 9, "privilege": "admin"}, "organization": {"id": 173, "owner": {"id": 280}, "user": {"role": null}}}, "resource": {"id": 114, "owner": {"id": 9}, "organization": {"id": 351}, "project": {"owner": {"id": 462}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 72, "privilege": "business"}, "organization": {"id": 138, "owner": {"id": 72}, "user": {"role": "owner"}}}, "resource": {"id": 187, "owner": {"id": 72}, "organization": {"id": 138}, "project": {"owner": {"id": 419}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 53, "privilege": "business"}, "organization": {"id": 177, "owner": {"id": 53}, "user": {"role": "owner"}}}, "resource": {"id": 131, "owner": {"id": 53}, "organization": {"id": 371}, "project": {"owner": {"id": 497}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 48, "privilege": "business"}, "organization": {"id": 157, "owner": {"id": 256}, "user": {"role": "maintainer"}}}, "resource": {"id": 179, "owner": {"id": 48}, "organization": {"id": 157}, "project": {"owner": {"id": 466}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 72, "privilege": "business"}, "organization": {"id": 179, "owner": {"id": 207}, "user": {"role": "maintainer"}}}, "resource": {"id": 138, "owner": {"id": 72}, "organization": {"id": 354}, "project": {"owner": {"id": 439}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 26, "privilege": "business"}, "organization": {"id": 180, "owner": {"id": 227}, "user": {"role": "supervisor"}}}, "resource": {"id": 178, "owner": {"id": 26}, "organization": {"id": 180}, "project": {"owner": {"id": 497}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 30, "privilege": "business"}, "organization": {"id": 122, "owner": {"id": 270}, "user": {"role": "supervisor"}}}, "resource": {"id": 133, "owner": {"id": 30}, "organization": {"id": 310}, "project": {"owner": {"id": 420}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 57, "privilege": "business"}, "organization": {"id": 188, "owner": {"id": 276}, "user": {"role": "worker"}}}, "resource": {"id": 109, "owner": {"id": 57}, "organization": {"id": 188}, "project": {"owner": {"id": 452}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 36, "privilege": "business"}, "organization": {"id": 190, "owner": {"id": 236}, "user": {"role": "worker"}}}, "resource": {"id": 160, "owner": {"id": 36}, "organization": {"id": 304}, "project": {"owner": {"id": 429}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 29, "privilege": "business"}, "organization": {"id": 133, "owner": {"id": 280}, "user": {"role": null}}}, "resource": {"id": 189, "owner": {"id": 29}, "organization": {"id": 133}, "project": {"owner": {"id": 487}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 14, "privilege": "business"}, "organization": {"id": 169, "owner": {"id": 228}, "user": {"role": null}}}, "resource": {"id": 175, "owner": {"id": 14}, "organization": {"id": 325}, "project": {"owner": {"id": 454}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 9, "privilege": "user"}, "organization": {"id": 107, "owner": {"id": 9}, "user": {"role": "owner"}}}, "resource": {"id": 182, "owner": {"id": 9}, "organization": {"id": 107}, "project": {"owner": {"id": 418}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 36, "privilege": "user"}, "organization": {"id": 156, "owner": {"id": 36}, "user": {"role": "owner"}}}, "resource": {"id": 139, "owner": {"id": 36}, "organization": {"id": 395}, "project": {"owner": {"id": 472}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 51, "privilege": "user"}, "organization": {"id": 134, "owner": {"id": 264}, "user": {"role": "maintainer"}}}, "resource": {"id": 159, "owner": {"id": 51}, "organization": {"id": 134}, "project": {"owner": {"id": 489}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 76, "privilege": "user"}, "organization": {"id": 105, "owner": {"id": 255}, "user": {"role": "maintainer"}}}, "resource": {"id": 169, "owner": {"id": 76}, "organization": {"id": 356}, "project": {"owner": {"id": 410}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 3, "privilege": "user"}, "organization": {"id": 111, "owner": {"id": 229}, "user": {"role": "supervisor"}}}, "resource": {"id": 194, "owner": {"id": 3}, "organization": {"id": 111}, "project": {"owner": {"id": 432}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 97, "privilege": "user"}, "organization": {"id": 186, "owner": {"id": 234}, "user": {"role": "supervisor"}}}, "resource": {"id": 186, "owner": {"id": 97}, "organization": {"id": 375}, "project": {"owner": {"id": 402}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 22, "privilege": "user"}, "organization": {"id": 160, "owner": {"id": 266}, "user": {"role": "worker"}}}, "resource": {"id": 173, "owner": {"id": 22}, "organization": {"id": 160}, "project": {"owner": {"id": 496}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 74, "privilege": "user"}, "organization": {"id": 155, "owner": {"id": 281}, "user": {"role": "worker"}}}, "resource": {"id": 183, "owner": {"id": 74}, "organization": {"id": 335}, "project": {"owner": {"id": 423}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 52, "privilege": "user"}, "organization": {"id": 142, "owner": {"id": 241}, "user": {"role": null}}}, "resource": {"id": 162, "owner": {"id": 52}, "organization": {"id": 142}, "project": {"owner": {"id": 444}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 52, "privilege": "user"}, "organization": {"id": 188, "owner": {"id": 263}, "user": {"role": null}}}, "resource": {"id": 185, "owner": {"id": 52}, "organization": {"id": 320}, "project": {"owner": {"id": 442}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 70, "privilege": "worker"}, "organization": {"id": 104, "owner": {"id": 70}, "user": {"role": "owner"}}}, "resource": {"id": 136, "owner": {"id": 70}, "organization": {"id": 104}, "project": {"owner": {"id": 497}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 14, "privilege": "worker"}, "organization": {"id": 198, "owner": {"id": 14}, "user": {"role": "owner"}}}, "resource": {"id": 111, "owner": {"id": 14}, "organization": {"id": 332}, "project": {"owner": {"id": 441}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 59, "privilege": "worker"}, "organization": {"id": 152, "owner": {"id": 206}, "user": {"role": "maintainer"}}}, "resource": {"id": 165, "owner": {"id": 59}, "organization": {"id": 152}, "project": {"owner": {"id": 469}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 96, "privilege": "worker"}, "organization": {"id": 163, "owner": {"id": 280}, "user": {"role": "maintainer"}}}, "resource": {"id": 124, "owner": {"id": 96}, "organization": {"id": 346}, "project": {"owner": {"id": 479}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 34, "privilege": "worker"}, "organization": {"id": 170, "owner": {"id": 216}, "user": {"role": "supervisor"}}}, "resource": {"id": 156, "owner": {"id": 34}, "organization": {"id": 170}, "project": {"owner": {"id": 426}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 15, "privilege": "worker"}, "organization": {"id": 103, "owner": {"id": 280}, "user": {"role": "supervisor"}}}, "resource": {"id": 136, "owner": {"id": 15}, "organization": {"id": 389}, "project": {"owner": {"id": 462}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 39, "privilege": "worker"}, "organization": {"id": 170, "owner": {"id": 201}, "user": {"role": "worker"}}}, "resource": {"id": 177, "owner": {"id": 39}, "organization": {"id": 170}, "project": {"owner": {"id": 420}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 14, "privilege": "worker"}, "organization": {"id": 159, "owner": {"id": 215}, "user": {"role": "worker"}}}, "resource": {"id": 170, "owner": {"id": 14}, "organization": {"id": 311}, "project": {"owner": {"id": 428}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 37, "privilege": "worker"}, "organization": {"id": 165, "owner": {"id": 290}, "user": {"role": null}}}, "resource": {"id": 182, "owner": {"id": 37}, "organization": {"id": 165}, "project": {"owner": {"id": 491}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 31, "privilege": "worker"}, "organization": {"id": 158, "owner": {"id": 270}, "user": {"role": null}}}, "resource": {"id": 134, "owner": {"id": 31}, "organization": {"id": 361}, "project": {"owner": {"id": 460}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 65, "privilege": "none"}, "organization": {"id": 195, "owner": {"id": 65}, "user": {"role": "owner"}}}, "resource": {"id": 118, "owner": {"id": 65}, "organization": {"id": 195}, "project": {"owner": {"id": 476}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 43, "privilege": "none"}, "organization": {"id": 164, "owner": {"id": 43}, "user": {"role": "owner"}}}, "resource": {"id": 108, "owner": {"id": 43}, "organization": {"id": 398}, "project": {"owner": {"id": 453}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 75, "privilege": "none"}, "organization": {"id": 174, "owner": {"id": 284}, "user": {"role": "maintainer"}}}, "resource": {"id": 100, "owner": {"id": 75}, "organization": {"id": 174}, "project": {"owner": {"id": 438}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 61, "privilege": "none"}, "organization": {"id": 144, "owner": {"id": 242}, "user": {"role": "maintainer"}}}, "resource": {"id": 162, "owner": {"id": 61}, "organization": {"id": 357}, "project": {"owner": {"id": 468}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 58, "privilege": "none"}, "organization": {"id": 141, "owner": {"id": 224}, "user": {"role": "supervisor"}}}, "resource": {"id": 170, "owner": {"id": 58}, "organization": {"id": 141}, "project": {"owner": {"id": 448}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 29, "privilege": "none"}, "organization": {"id": 199, "owner": {"id": 252}, "user": {"role": "supervisor"}}}, "resource": {"id": 189, "owner": {"id": 29}, "organization": {"id": 373}, "project": {"owner": {"id": 449}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 90, "privilege": "none"}, "organization": {"id": 148, "owner": {"id": 249}, "user": {"role": "worker"}}}, "resource": {"id": 105, "owner": {"id": 90}, "organization": {"id": 148}, "project": {"owner": {"id": 460}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 4, "privilege": "none"}, "organization": {"id": 116, "owner": {"id": 264}, "user": {"role": "worker"}}}, "resource": {"id": 184, "owner": {"id": 4}, "organization": {"id": 319}, "project": {"owner": {"id": 463}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 12, "privilege": "none"}, "organization": {"id": 167, "owner": {"id": 258}, "user": {"role": null}}}, "resource": {"id": 175, "owner": {"id": 12}, "organization": {"id": 167}, "project": {"owner": {"id": 456}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 83, "privilege": "none"}, "organization": {"id": 119, "owner": {"id": 209}, "user": {"role": null}}}, "resource": {"id": 101, "owner": {"id": 83}, "organization": {"id": 318}, "project": {"owner": {"id": 452}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 88, "privilege": "admin"}, "organization": {"id": 150, "owner": {"id": 88}, "user": {"role": "owner"}}}, "resource": {"id": 160, "owner": {"id": 233}, "organization": {"id": 150}, "project": {"owner": {"id": 479}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_OWNER_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 48, "privilege": "admin"}, "organization": {"id": 140, "owner": {"id": 48}, "user": {"role": "owner"}}}, "resource": {"id": 110, "owner": {"id": 242}, "organization": {"id": 386}, "project": {"owner": {"id": 468}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 4, "privilege": "admin"}, "organization": {"id": 179, "owner": {"id": 208}, "user": {"role": "maintainer"}}}, "resource": {"id": 191, "owner": {"id": 297}, "organization": {"id": 179}, "project": {"owner": {"id": 469}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 29, "privilege": "admin"}, "organization": {"id": 195, "owner": {"id": 211}, "user": {"role": "maintainer"}}}, "resource": {"id": 130, "owner": {"id": 280}, "organization": {"id": 387}, "project": {"owner": {"id": 436}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 90, "privilege": "admin"}, "organization": {"id": 112, "owner": {"id": 256}, "user": {"role": "supervisor"}}}, "resource": {"id": 155, "owner": {"id": 212}, "organization": {"id": 112}, "project": {"owner": {"id": 481}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 5, "privilege": "admin"}, "organization": {"id": 141, "owner": {"id": 207}, "user": {"role": "supervisor"}}}, "resource": {"id": 121, "owner": {"id": 288}, "organization": {"id": 338}, "project": {"owner": {"id": 403}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 18, "privilege": "admin"}, "organization": {"id": 131, "owner": {"id": 267}, "user": {"role": "worker"}}}, "resource": {"id": 137, "owner": {"id": 245}, "organization": {"id": 131}, "project": {"owner": {"id": 455}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_WORKER_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 21, "privilege": "admin"}, "organization": {"id": 122, "owner": {"id": 210}, "user": {"role": "worker"}}}, "resource": {"id": 152, "owner": {"id": 272}, "organization": {"id": 387}, "project": {"owner": {"id": 423}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 30, "privilege": "admin"}, "organization": {"id": 163, "owner": {"id": 274}, "user": {"role": null}}}, "resource": {"id": 178, "owner": {"id": 248}, "organization": {"id": 163}, "project": {"owner": {"id": 487}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_NONE_resource_project_same_org_FALSE { + allow with input as {"scope": "view", "auth": {"user": {"id": 32, "privilege": "admin"}, "organization": {"id": 158, "owner": {"id": 232}, "user": {"role": null}}}, "resource": {"id": 118, "owner": {"id": 229}, "organization": {"id": 359}, "project": {"owner": {"id": 481}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 86, "privilege": "business"}, "organization": {"id": 169, "owner": {"id": 86}, "user": {"role": "owner"}}}, "resource": {"id": 185, "owner": {"id": 201}, "organization": {"id": 169}, "project": {"owner": {"id": 436}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 38, "privilege": "business"}, "organization": {"id": 181, "owner": {"id": 38}, "user": {"role": "owner"}}}, "resource": {"id": 109, "owner": {"id": 256}, "organization": {"id": 344}, "project": {"owner": {"id": 475}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 25, "privilege": "business"}, "organization": {"id": 149, "owner": {"id": 261}, "user": {"role": "maintainer"}}}, "resource": {"id": 188, "owner": {"id": 232}, "organization": {"id": 149}, "project": {"owner": {"id": 438}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 45, "privilege": "business"}, "organization": {"id": 173, "owner": {"id": 237}, "user": {"role": "maintainer"}}}, "resource": {"id": 113, "owner": {"id": 230}, "organization": {"id": 348}, "project": {"owner": {"id": 473}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 50, "privilege": "business"}, "organization": {"id": 135, "owner": {"id": 201}, "user": {"role": "supervisor"}}}, "resource": {"id": 189, "owner": {"id": 237}, "organization": {"id": 135}, "project": {"owner": {"id": 484}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 6, "privilege": "business"}, "organization": {"id": 177, "owner": {"id": 295}, "user": {"role": "supervisor"}}}, "resource": {"id": 172, "owner": {"id": 287}, "organization": {"id": 399}, "project": {"owner": {"id": 495}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 77, "privilege": "business"}, "organization": {"id": 145, "owner": {"id": 228}, "user": {"role": "worker"}}}, "resource": {"id": 163, "owner": {"id": 236}, "organization": {"id": 145}, "project": {"owner": {"id": 429}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 86, "privilege": "business"}, "organization": {"id": 196, "owner": {"id": 292}, "user": {"role": "worker"}}}, "resource": {"id": 181, "owner": {"id": 224}, "organization": {"id": 379}, "project": {"owner": {"id": 432}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 80, "privilege": "business"}, "organization": {"id": 112, "owner": {"id": 280}, "user": {"role": null}}}, "resource": {"id": 198, "owner": {"id": 284}, "organization": {"id": 112}, "project": {"owner": {"id": 417}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 4, "privilege": "business"}, "organization": {"id": 174, "owner": {"id": 246}, "user": {"role": null}}}, "resource": {"id": 182, "owner": {"id": 205}, "organization": {"id": 339}, "project": {"owner": {"id": 456}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 41, "privilege": "user"}, "organization": {"id": 195, "owner": {"id": 41}, "user": {"role": "owner"}}}, "resource": {"id": 193, "owner": {"id": 216}, "organization": {"id": 195}, "project": {"owner": {"id": 437}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 46, "privilege": "user"}, "organization": {"id": 167, "owner": {"id": 46}, "user": {"role": "owner"}}}, "resource": {"id": 122, "owner": {"id": 225}, "organization": {"id": 316}, "project": {"owner": {"id": 469}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 37, "privilege": "user"}, "organization": {"id": 195, "owner": {"id": 243}, "user": {"role": "maintainer"}}}, "resource": {"id": 134, "owner": {"id": 221}, "organization": {"id": 195}, "project": {"owner": {"id": 461}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 96, "privilege": "user"}, "organization": {"id": 128, "owner": {"id": 286}, "user": {"role": "maintainer"}}}, "resource": {"id": 114, "owner": {"id": 259}, "organization": {"id": 309}, "project": {"owner": {"id": 418}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 46, "privilege": "user"}, "organization": {"id": 111, "owner": {"id": 250}, "user": {"role": "supervisor"}}}, "resource": {"id": 192, "owner": {"id": 286}, "organization": {"id": 111}, "project": {"owner": {"id": 471}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 58, "privilege": "user"}, "organization": {"id": 147, "owner": {"id": 286}, "user": {"role": "supervisor"}}}, "resource": {"id": 101, "owner": {"id": 233}, "organization": {"id": 368}, "project": {"owner": {"id": 415}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 48, "privilege": "user"}, "organization": {"id": 181, "owner": {"id": 247}, "user": {"role": "worker"}}}, "resource": {"id": 195, "owner": {"id": 286}, "organization": {"id": 181}, "project": {"owner": {"id": 474}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 3, "privilege": "user"}, "organization": {"id": 179, "owner": {"id": 271}, "user": {"role": "worker"}}}, "resource": {"id": 113, "owner": {"id": 286}, "organization": {"id": 329}, "project": {"owner": {"id": 460}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 8, "privilege": "user"}, "organization": {"id": 181, "owner": {"id": 259}, "user": {"role": null}}}, "resource": {"id": 141, "owner": {"id": 278}, "organization": {"id": 181}, "project": {"owner": {"id": 482}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 14, "privilege": "user"}, "organization": {"id": 117, "owner": {"id": 205}, "user": {"role": null}}}, "resource": {"id": 189, "owner": {"id": 238}, "organization": {"id": 383}, "project": {"owner": {"id": 452}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 12, "privilege": "worker"}, "organization": {"id": 130, "owner": {"id": 12}, "user": {"role": "owner"}}}, "resource": {"id": 104, "owner": {"id": 238}, "organization": {"id": 130}, "project": {"owner": {"id": 414}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 85, "privilege": "worker"}, "organization": {"id": 195, "owner": {"id": 85}, "user": {"role": "owner"}}}, "resource": {"id": 117, "owner": {"id": 249}, "organization": {"id": 358}, "project": {"owner": {"id": 447}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "view", "auth": {"user": {"id": 93, "privilege": "worker"}, "organization": {"id": 119, "owner": {"id": 253}, "user": {"role": "maintainer"}}}, "resource": {"id": 169, "owner": {"id": 253}, "organization": {"id": 119}, "project": {"owner": {"id": 495}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 52, "privilege": "worker"}, "organization": {"id": 135, "owner": {"id": 204}, "user": {"role": "maintainer"}}}, "resource": {"id": 183, "owner": {"id": 212}, "organization": {"id": 362}, "project": {"owner": {"id": 478}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 56, "privilege": "worker"}, "organization": {"id": 130, "owner": {"id": 246}, "user": {"role": "supervisor"}}}, "resource": {"id": 188, "owner": {"id": 247}, "organization": {"id": 130}, "project": {"owner": {"id": 456}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 82, "privilege": "worker"}, "organization": {"id": 145, "owner": {"id": 207}, "user": {"role": "supervisor"}}}, "resource": {"id": 112, "owner": {"id": 287}, "organization": {"id": 347}, "project": {"owner": {"id": 469}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 58, "privilege": "worker"}, "organization": {"id": 111, "owner": {"id": 284}, "user": {"role": "worker"}}}, "resource": {"id": 150, "owner": {"id": 235}, "organization": {"id": 111}, "project": {"owner": {"id": 415}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 2, "privilege": "worker"}, "organization": {"id": 106, "owner": {"id": 242}, "user": {"role": "worker"}}}, "resource": {"id": 127, "owner": {"id": 282}, "organization": {"id": 381}, "project": {"owner": {"id": 476}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 8, "privilege": "worker"}, "organization": {"id": 197, "owner": {"id": 270}, "user": {"role": null}}}, "resource": {"id": 131, "owner": {"id": 216}, "organization": {"id": 197}, "project": {"owner": {"id": 426}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 42, "privilege": "worker"}, "organization": {"id": 199, "owner": {"id": 218}, "user": {"role": null}}}, "resource": {"id": 126, "owner": {"id": 275}, "organization": {"id": 327}, "project": {"owner": {"id": 429}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_OWNER_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 16, "privilege": "none"}, "organization": {"id": 169, "owner": {"id": 16}, "user": {"role": "owner"}}}, "resource": {"id": 176, "owner": {"id": 200}, "organization": {"id": 169}, "project": {"owner": {"id": 418}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 16, "privilege": "none"}, "organization": {"id": 101, "owner": {"id": 16}, "user": {"role": "owner"}}}, "resource": {"id": 122, "owner": {"id": 214}, "organization": {"id": 384}, "project": {"owner": {"id": 403}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_MAINTAINER_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 22, "privilege": "none"}, "organization": {"id": 133, "owner": {"id": 206}, "user": {"role": "maintainer"}}}, "resource": {"id": 130, "owner": {"id": 275}, "organization": {"id": 133}, "project": {"owner": {"id": 402}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 14, "privilege": "none"}, "organization": {"id": 195, "owner": {"id": 208}, "user": {"role": "maintainer"}}}, "resource": {"id": 116, "owner": {"id": 294}, "organization": {"id": 353}, "project": {"owner": {"id": 467}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 65, "privilege": "none"}, "organization": {"id": 175, "owner": {"id": 213}, "user": {"role": "supervisor"}}}, "resource": {"id": 160, "owner": {"id": 257}, "organization": {"id": 175}, "project": {"owner": {"id": 446}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 5, "privilege": "none"}, "organization": {"id": 193, "owner": {"id": 284}, "user": {"role": "supervisor"}}}, "resource": {"id": 157, "owner": {"id": 264}, "organization": {"id": 328}, "project": {"owner": {"id": 478}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 3, "privilege": "none"}, "organization": {"id": 107, "owner": {"id": 261}, "user": {"role": "worker"}}}, "resource": {"id": 166, "owner": {"id": 238}, "organization": {"id": 107}, "project": {"owner": {"id": 482}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 62, "privilege": "none"}, "organization": {"id": 191, "owner": {"id": 256}, "user": {"role": "worker"}}}, "resource": {"id": 151, "owner": {"id": 254}, "organization": {"id": 387}, "project": {"owner": {"id": 413}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 18, "privilege": "none"}, "organization": {"id": 108, "owner": {"id": 216}, "user": {"role": null}}}, "resource": {"id": 109, "owner": {"id": 210}, "organization": {"id": 108}, "project": {"owner": {"id": 477}}}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 70, "privilege": "none"}, "organization": {"id": 191, "owner": {"id": 241}, "user": {"role": null}}}, "resource": {"id": 135, "owner": {"id": 279}, "organization": {"id": 381}, "project": {"owner": {"id": 474}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 98, "privilege": "admin"}, "organization": null}, "resource": {"id": 109, "owner": {"id": 225}, "organization": {"id": 317}, "project": {"owner": {"id": 98}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 22, "privilege": "business"}, "organization": null}, "resource": {"id": 106, "owner": {"id": 286}, "organization": {"id": 338}, "project": {"owner": {"id": 22}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 21, "privilege": "user"}, "organization": null}, "resource": {"id": 115, "owner": {"id": 205}, "organization": {"id": 359}, "project": {"owner": {"id": 21}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 2, "privilege": "worker"}, "organization": null}, "resource": {"id": 167, "owner": {"id": 231}, "organization": {"id": 380}, "project": {"owner": {"id": 2}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 45, "privilege": "none"}, "organization": null}, "resource": {"id": 197, "owner": {"id": 228}, "organization": {"id": 373}, "project": {"owner": {"id": 45}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 41, "privilege": "admin"}, "organization": null}, "resource": {"id": 116, "owner": {"id": 41}, "organization": {"id": 391}, "project": {"owner": {"id": 422}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 93, "privilege": "business"}, "organization": null}, "resource": {"id": 175, "owner": {"id": 93}, "organization": {"id": 348}, "project": {"owner": {"id": 439}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 31, "privilege": "user"}, "organization": null}, "resource": {"id": 157, "owner": {"id": 31}, "organization": {"id": 333}, "project": {"owner": {"id": 426}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 93, "privilege": "worker"}, "organization": null}, "resource": {"id": 143, "owner": {"id": 93}, "organization": {"id": 348}, "project": {"owner": {"id": 420}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 0, "privilege": "none"}, "organization": null}, "resource": {"id": 162, "owner": {"id": 0}, "organization": {"id": 306}, "project": {"owner": {"id": 446}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_NONE_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 72, "privilege": "admin"}, "organization": null}, "resource": {"id": 159, "owner": {"id": 233}, "organization": {"id": 328}, "project": {"owner": {"id": 434}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_NONE_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 22, "privilege": "business"}, "organization": null}, "resource": {"id": 118, "owner": {"id": 215}, "organization": {"id": 396}, "project": {"owner": {"id": 457}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_NONE_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 12, "privilege": "user"}, "organization": null}, "resource": {"id": 172, "owner": {"id": 275}, "organization": {"id": 374}, "project": {"owner": {"id": 497}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_NONE_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 62, "privilege": "worker"}, "organization": null}, "resource": {"id": 143, "owner": {"id": 238}, "organization": {"id": 339}, "project": {"owner": {"id": 435}}}} +} + +test_scope_DELETE_context_SANDBOX_ownership_NONE_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 25, "privilege": "none"}, "organization": null}, "resource": {"id": 172, "owner": {"id": 203}, "organization": {"id": 322}, "project": {"owner": {"id": 424}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 35, "privilege": "admin"}, "organization": {"id": 147, "owner": {"id": 35}, "user": {"role": "owner"}}}, "resource": {"id": 106, "owner": {"id": 246}, "organization": {"id": 147}, "project": {"owner": {"id": 35}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 87, "privilege": "admin"}, "organization": {"id": 103, "owner": {"id": 87}, "user": {"role": "owner"}}}, "resource": {"id": 164, "owner": {"id": 250}, "organization": {"id": 381}, "project": {"owner": {"id": 87}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 22, "privilege": "admin"}, "organization": {"id": 160, "owner": {"id": 275}, "user": {"role": "maintainer"}}}, "resource": {"id": 147, "owner": {"id": 262}, "organization": {"id": 160}, "project": {"owner": {"id": 22}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 33, "privilege": "admin"}, "organization": {"id": 195, "owner": {"id": 288}, "user": {"role": "maintainer"}}}, "resource": {"id": 170, "owner": {"id": 244}, "organization": {"id": 344}, "project": {"owner": {"id": 33}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 6, "privilege": "admin"}, "organization": {"id": 120, "owner": {"id": 273}, "user": {"role": "supervisor"}}}, "resource": {"id": 111, "owner": {"id": 236}, "organization": {"id": 120}, "project": {"owner": {"id": 6}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 28, "privilege": "admin"}, "organization": {"id": 186, "owner": {"id": 226}, "user": {"role": "supervisor"}}}, "resource": {"id": 199, "owner": {"id": 227}, "organization": {"id": 328}, "project": {"owner": {"id": 28}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 2, "privilege": "admin"}, "organization": {"id": 199, "owner": {"id": 298}, "user": {"role": "worker"}}}, "resource": {"id": 134, "owner": {"id": 283}, "organization": {"id": 199}, "project": {"owner": {"id": 2}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 82, "privilege": "admin"}, "organization": {"id": 122, "owner": {"id": 276}, "user": {"role": "worker"}}}, "resource": {"id": 187, "owner": {"id": 201}, "organization": {"id": 360}, "project": {"owner": {"id": 82}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 39, "privilege": "admin"}, "organization": {"id": 191, "owner": {"id": 290}, "user": {"role": null}}}, "resource": {"id": 101, "owner": {"id": 228}, "organization": {"id": 191}, "project": {"owner": {"id": 39}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 44, "privilege": "admin"}, "organization": {"id": 158, "owner": {"id": 232}, "user": {"role": null}}}, "resource": {"id": 182, "owner": {"id": 235}, "organization": {"id": 354}, "project": {"owner": {"id": 44}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 66, "privilege": "business"}, "organization": {"id": 179, "owner": {"id": 66}, "user": {"role": "owner"}}}, "resource": {"id": 127, "owner": {"id": 259}, "organization": {"id": 179}, "project": {"owner": {"id": 66}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 84, "privilege": "business"}, "organization": {"id": 147, "owner": {"id": 84}, "user": {"role": "owner"}}}, "resource": {"id": 174, "owner": {"id": 213}, "organization": {"id": 300}, "project": {"owner": {"id": 84}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 38, "privilege": "business"}, "organization": {"id": 187, "owner": {"id": 213}, "user": {"role": "maintainer"}}}, "resource": {"id": 182, "owner": {"id": 276}, "organization": {"id": 187}, "project": {"owner": {"id": 38}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 81, "privilege": "business"}, "organization": {"id": 141, "owner": {"id": 235}, "user": {"role": "maintainer"}}}, "resource": {"id": 161, "owner": {"id": 208}, "organization": {"id": 343}, "project": {"owner": {"id": 81}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 38, "privilege": "business"}, "organization": {"id": 124, "owner": {"id": 219}, "user": {"role": "supervisor"}}}, "resource": {"id": 133, "owner": {"id": 283}, "organization": {"id": 124}, "project": {"owner": {"id": 38}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 76, "privilege": "business"}, "organization": {"id": 151, "owner": {"id": 284}, "user": {"role": "supervisor"}}}, "resource": {"id": 166, "owner": {"id": 297}, "organization": {"id": 330}, "project": {"owner": {"id": 76}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 3, "privilege": "business"}, "organization": {"id": 183, "owner": {"id": 263}, "user": {"role": "worker"}}}, "resource": {"id": 140, "owner": {"id": 287}, "organization": {"id": 183}, "project": {"owner": {"id": 3}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 49, "privilege": "business"}, "organization": {"id": 194, "owner": {"id": 204}, "user": {"role": "worker"}}}, "resource": {"id": 137, "owner": {"id": 233}, "organization": {"id": 353}, "project": {"owner": {"id": 49}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 43, "privilege": "business"}, "organization": {"id": 192, "owner": {"id": 291}, "user": {"role": null}}}, "resource": {"id": 174, "owner": {"id": 290}, "organization": {"id": 192}, "project": {"owner": {"id": 43}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 60, "privilege": "business"}, "organization": {"id": 185, "owner": {"id": 245}, "user": {"role": null}}}, "resource": {"id": 128, "owner": {"id": 287}, "organization": {"id": 368}, "project": {"owner": {"id": 60}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 71, "privilege": "user"}, "organization": {"id": 123, "owner": {"id": 71}, "user": {"role": "owner"}}}, "resource": {"id": 164, "owner": {"id": 238}, "organization": {"id": 123}, "project": {"owner": {"id": 71}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 33, "privilege": "user"}, "organization": {"id": 192, "owner": {"id": 33}, "user": {"role": "owner"}}}, "resource": {"id": 112, "owner": {"id": 260}, "organization": {"id": 316}, "project": {"owner": {"id": 33}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 83, "privilege": "user"}, "organization": {"id": 111, "owner": {"id": 228}, "user": {"role": "maintainer"}}}, "resource": {"id": 197, "owner": {"id": 223}, "organization": {"id": 111}, "project": {"owner": {"id": 83}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 53, "privilege": "user"}, "organization": {"id": 142, "owner": {"id": 247}, "user": {"role": "maintainer"}}}, "resource": {"id": 145, "owner": {"id": 228}, "organization": {"id": 395}, "project": {"owner": {"id": 53}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 15, "privilege": "user"}, "organization": {"id": 160, "owner": {"id": 206}, "user": {"role": "supervisor"}}}, "resource": {"id": 133, "owner": {"id": 274}, "organization": {"id": 160}, "project": {"owner": {"id": 15}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 9, "privilege": "user"}, "organization": {"id": 160, "owner": {"id": 224}, "user": {"role": "supervisor"}}}, "resource": {"id": 181, "owner": {"id": 274}, "organization": {"id": 374}, "project": {"owner": {"id": 9}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 67, "privilege": "user"}, "organization": {"id": 138, "owner": {"id": 252}, "user": {"role": "worker"}}}, "resource": {"id": 164, "owner": {"id": 215}, "organization": {"id": 138}, "project": {"owner": {"id": 67}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 43, "privilege": "user"}, "organization": {"id": 152, "owner": {"id": 272}, "user": {"role": "worker"}}}, "resource": {"id": 106, "owner": {"id": 219}, "organization": {"id": 317}, "project": {"owner": {"id": 43}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 23, "privilege": "user"}, "organization": {"id": 199, "owner": {"id": 210}, "user": {"role": null}}}, "resource": {"id": 158, "owner": {"id": 218}, "organization": {"id": 199}, "project": {"owner": {"id": 23}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 40, "privilege": "user"}, "organization": {"id": 181, "owner": {"id": 207}, "user": {"role": null}}}, "resource": {"id": 162, "owner": {"id": 242}, "organization": {"id": 380}, "project": {"owner": {"id": 40}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 97, "privilege": "worker"}, "organization": {"id": 199, "owner": {"id": 97}, "user": {"role": "owner"}}}, "resource": {"id": 100, "owner": {"id": 257}, "organization": {"id": 199}, "project": {"owner": {"id": 97}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 97, "privilege": "worker"}, "organization": {"id": 199, "owner": {"id": 97}, "user": {"role": "owner"}}}, "resource": {"id": 173, "owner": {"id": 297}, "organization": {"id": 320}, "project": {"owner": {"id": 97}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 54, "privilege": "worker"}, "organization": {"id": 151, "owner": {"id": 254}, "user": {"role": "maintainer"}}}, "resource": {"id": 116, "owner": {"id": 278}, "organization": {"id": 151}, "project": {"owner": {"id": 54}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 68, "privilege": "worker"}, "organization": {"id": 125, "owner": {"id": 293}, "user": {"role": "maintainer"}}}, "resource": {"id": 161, "owner": {"id": 249}, "organization": {"id": 300}, "project": {"owner": {"id": 68}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 67, "privilege": "worker"}, "organization": {"id": 124, "owner": {"id": 202}, "user": {"role": "supervisor"}}}, "resource": {"id": 147, "owner": {"id": 201}, "organization": {"id": 124}, "project": {"owner": {"id": 67}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 31, "privilege": "worker"}, "organization": {"id": 128, "owner": {"id": 288}, "user": {"role": "supervisor"}}}, "resource": {"id": 186, "owner": {"id": 200}, "organization": {"id": 380}, "project": {"owner": {"id": 31}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 49, "privilege": "worker"}, "organization": {"id": 164, "owner": {"id": 275}, "user": {"role": "worker"}}}, "resource": {"id": 144, "owner": {"id": 239}, "organization": {"id": 164}, "project": {"owner": {"id": 49}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 38, "privilege": "worker"}, "organization": {"id": 137, "owner": {"id": 258}, "user": {"role": "worker"}}}, "resource": {"id": 139, "owner": {"id": 221}, "organization": {"id": 308}, "project": {"owner": {"id": 38}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 43, "privilege": "worker"}, "organization": {"id": 155, "owner": {"id": 287}, "user": {"role": null}}}, "resource": {"id": 193, "owner": {"id": 266}, "organization": {"id": 155}, "project": {"owner": {"id": 43}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 96, "privilege": "worker"}, "organization": {"id": 124, "owner": {"id": 221}, "user": {"role": null}}}, "resource": {"id": 117, "owner": {"id": 243}, "organization": {"id": 362}, "project": {"owner": {"id": 96}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 93, "privilege": "none"}, "organization": {"id": 116, "owner": {"id": 93}, "user": {"role": "owner"}}}, "resource": {"id": 151, "owner": {"id": 202}, "organization": {"id": 116}, "project": {"owner": {"id": 93}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 21, "privilege": "none"}, "organization": {"id": 115, "owner": {"id": 21}, "user": {"role": "owner"}}}, "resource": {"id": 193, "owner": {"id": 202}, "organization": {"id": 375}, "project": {"owner": {"id": 21}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 81, "privilege": "none"}, "organization": {"id": 132, "owner": {"id": 298}, "user": {"role": "maintainer"}}}, "resource": {"id": 191, "owner": {"id": 283}, "organization": {"id": 132}, "project": {"owner": {"id": 81}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 6, "privilege": "none"}, "organization": {"id": 184, "owner": {"id": 213}, "user": {"role": "maintainer"}}}, "resource": {"id": 168, "owner": {"id": 279}, "organization": {"id": 306}, "project": {"owner": {"id": 6}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 14, "privilege": "none"}, "organization": {"id": 153, "owner": {"id": 256}, "user": {"role": "supervisor"}}}, "resource": {"id": 183, "owner": {"id": 202}, "organization": {"id": 153}, "project": {"owner": {"id": 14}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 60, "privilege": "none"}, "organization": {"id": 189, "owner": {"id": 219}, "user": {"role": "supervisor"}}}, "resource": {"id": 148, "owner": {"id": 215}, "organization": {"id": 370}, "project": {"owner": {"id": 60}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 1, "privilege": "none"}, "organization": {"id": 138, "owner": {"id": 253}, "user": {"role": "worker"}}}, "resource": {"id": 126, "owner": {"id": 288}, "organization": {"id": 138}, "project": {"owner": {"id": 1}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 34, "privilege": "none"}, "organization": {"id": 179, "owner": {"id": 277}, "user": {"role": "worker"}}}, "resource": {"id": 184, "owner": {"id": 212}, "organization": {"id": 384}, "project": {"owner": {"id": 34}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 65, "privilege": "none"}, "organization": {"id": 192, "owner": {"id": 279}, "user": {"role": null}}}, "resource": {"id": 189, "owner": {"id": 217}, "organization": {"id": 192}, "project": {"owner": {"id": 65}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 63, "privilege": "none"}, "organization": {"id": 125, "owner": {"id": 277}, "user": {"role": null}}}, "resource": {"id": 115, "owner": {"id": 236}, "organization": {"id": 314}, "project": {"owner": {"id": 63}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 25, "privilege": "admin"}, "organization": {"id": 145, "owner": {"id": 25}, "user": {"role": "owner"}}}, "resource": {"id": 125, "owner": {"id": 25}, "organization": {"id": 145}, "project": {"owner": {"id": 466}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 70, "privilege": "admin"}, "organization": {"id": 163, "owner": {"id": 70}, "user": {"role": "owner"}}}, "resource": {"id": 152, "owner": {"id": 70}, "organization": {"id": 320}, "project": {"owner": {"id": 405}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 0, "privilege": "admin"}, "organization": {"id": 101, "owner": {"id": 285}, "user": {"role": "maintainer"}}}, "resource": {"id": 188, "owner": {"id": 0}, "organization": {"id": 101}, "project": {"owner": {"id": 430}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 63, "privilege": "admin"}, "organization": {"id": 119, "owner": {"id": 211}, "user": {"role": "maintainer"}}}, "resource": {"id": 111, "owner": {"id": 63}, "organization": {"id": 373}, "project": {"owner": {"id": 485}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 92, "privilege": "admin"}, "organization": {"id": 112, "owner": {"id": 232}, "user": {"role": "supervisor"}}}, "resource": {"id": 199, "owner": {"id": 92}, "organization": {"id": 112}, "project": {"owner": {"id": 492}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 33, "privilege": "admin"}, "organization": {"id": 159, "owner": {"id": 206}, "user": {"role": "supervisor"}}}, "resource": {"id": 182, "owner": {"id": 33}, "organization": {"id": 358}, "project": {"owner": {"id": 437}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 46, "privilege": "admin"}, "organization": {"id": 186, "owner": {"id": 240}, "user": {"role": "worker"}}}, "resource": {"id": 112, "owner": {"id": 46}, "organization": {"id": 186}, "project": {"owner": {"id": 437}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 97, "privilege": "admin"}, "organization": {"id": 105, "owner": {"id": 201}, "user": {"role": "worker"}}}, "resource": {"id": 154, "owner": {"id": 97}, "organization": {"id": 314}, "project": {"owner": {"id": 412}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 42, "privilege": "admin"}, "organization": {"id": 145, "owner": {"id": 256}, "user": {"role": null}}}, "resource": {"id": 117, "owner": {"id": 42}, "organization": {"id": 145}, "project": {"owner": {"id": 421}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 47, "privilege": "admin"}, "organization": {"id": 143, "owner": {"id": 223}, "user": {"role": null}}}, "resource": {"id": 179, "owner": {"id": 47}, "organization": {"id": 393}, "project": {"owner": {"id": 411}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 34, "privilege": "business"}, "organization": {"id": 149, "owner": {"id": 34}, "user": {"role": "owner"}}}, "resource": {"id": 114, "owner": {"id": 34}, "organization": {"id": 149}, "project": {"owner": {"id": 458}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 14, "privilege": "business"}, "organization": {"id": 116, "owner": {"id": 14}, "user": {"role": "owner"}}}, "resource": {"id": 161, "owner": {"id": 14}, "organization": {"id": 383}, "project": {"owner": {"id": 421}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 53, "privilege": "business"}, "organization": {"id": 175, "owner": {"id": 261}, "user": {"role": "maintainer"}}}, "resource": {"id": 192, "owner": {"id": 53}, "organization": {"id": 175}, "project": {"owner": {"id": 413}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 78, "privilege": "business"}, "organization": {"id": 148, "owner": {"id": 299}, "user": {"role": "maintainer"}}}, "resource": {"id": 172, "owner": {"id": 78}, "organization": {"id": 356}, "project": {"owner": {"id": 423}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 93, "privilege": "business"}, "organization": {"id": 116, "owner": {"id": 261}, "user": {"role": "supervisor"}}}, "resource": {"id": 145, "owner": {"id": 93}, "organization": {"id": 116}, "project": {"owner": {"id": 487}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 5, "privilege": "business"}, "organization": {"id": 108, "owner": {"id": 233}, "user": {"role": "supervisor"}}}, "resource": {"id": 162, "owner": {"id": 5}, "organization": {"id": 352}, "project": {"owner": {"id": 456}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 86, "privilege": "business"}, "organization": {"id": 167, "owner": {"id": 298}, "user": {"role": "worker"}}}, "resource": {"id": 183, "owner": {"id": 86}, "organization": {"id": 167}, "project": {"owner": {"id": 490}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 28, "privilege": "business"}, "organization": {"id": 185, "owner": {"id": 243}, "user": {"role": "worker"}}}, "resource": {"id": 195, "owner": {"id": 28}, "organization": {"id": 392}, "project": {"owner": {"id": 472}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 79, "privilege": "business"}, "organization": {"id": 112, "owner": {"id": 255}, "user": {"role": null}}}, "resource": {"id": 166, "owner": {"id": 79}, "organization": {"id": 112}, "project": {"owner": {"id": 489}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 44, "privilege": "business"}, "organization": {"id": 184, "owner": {"id": 286}, "user": {"role": null}}}, "resource": {"id": 184, "owner": {"id": 44}, "organization": {"id": 331}, "project": {"owner": {"id": 461}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 78, "privilege": "user"}, "organization": {"id": 106, "owner": {"id": 78}, "user": {"role": "owner"}}}, "resource": {"id": 182, "owner": {"id": 78}, "organization": {"id": 106}, "project": {"owner": {"id": 471}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 63, "privilege": "user"}, "organization": {"id": 121, "owner": {"id": 63}, "user": {"role": "owner"}}}, "resource": {"id": 182, "owner": {"id": 63}, "organization": {"id": 364}, "project": {"owner": {"id": 460}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 42, "privilege": "user"}, "organization": {"id": 135, "owner": {"id": 206}, "user": {"role": "maintainer"}}}, "resource": {"id": 196, "owner": {"id": 42}, "organization": {"id": 135}, "project": {"owner": {"id": 430}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 48, "privilege": "user"}, "organization": {"id": 151, "owner": {"id": 230}, "user": {"role": "maintainer"}}}, "resource": {"id": 164, "owner": {"id": 48}, "organization": {"id": 370}, "project": {"owner": {"id": 482}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 57, "privilege": "user"}, "organization": {"id": 111, "owner": {"id": 263}, "user": {"role": "supervisor"}}}, "resource": {"id": 110, "owner": {"id": 57}, "organization": {"id": 111}, "project": {"owner": {"id": 473}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 95, "privilege": "user"}, "organization": {"id": 183, "owner": {"id": 202}, "user": {"role": "supervisor"}}}, "resource": {"id": 157, "owner": {"id": 95}, "organization": {"id": 315}, "project": {"owner": {"id": 463}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 71, "privilege": "user"}, "organization": {"id": 170, "owner": {"id": 200}, "user": {"role": "worker"}}}, "resource": {"id": 113, "owner": {"id": 71}, "organization": {"id": 170}, "project": {"owner": {"id": 404}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 38, "privilege": "user"}, "organization": {"id": 165, "owner": {"id": 271}, "user": {"role": "worker"}}}, "resource": {"id": 111, "owner": {"id": 38}, "organization": {"id": 379}, "project": {"owner": {"id": 481}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 43, "privilege": "user"}, "organization": {"id": 146, "owner": {"id": 206}, "user": {"role": null}}}, "resource": {"id": 126, "owner": {"id": 43}, "organization": {"id": 146}, "project": {"owner": {"id": 458}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 98, "privilege": "user"}, "organization": {"id": 179, "owner": {"id": 276}, "user": {"role": null}}}, "resource": {"id": 128, "owner": {"id": 98}, "organization": {"id": 342}, "project": {"owner": {"id": 471}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 91, "privilege": "worker"}, "organization": {"id": 157, "owner": {"id": 91}, "user": {"role": "owner"}}}, "resource": {"id": 160, "owner": {"id": 91}, "organization": {"id": 157}, "project": {"owner": {"id": 446}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 0, "privilege": "worker"}, "organization": {"id": 143, "owner": {"id": 0}, "user": {"role": "owner"}}}, "resource": {"id": 112, "owner": {"id": 0}, "organization": {"id": 384}, "project": {"owner": {"id": 428}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 47, "privilege": "worker"}, "organization": {"id": 189, "owner": {"id": 213}, "user": {"role": "maintainer"}}}, "resource": {"id": 181, "owner": {"id": 47}, "organization": {"id": 189}, "project": {"owner": {"id": 466}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 91, "privilege": "worker"}, "organization": {"id": 195, "owner": {"id": 292}, "user": {"role": "maintainer"}}}, "resource": {"id": 105, "owner": {"id": 91}, "organization": {"id": 363}, "project": {"owner": {"id": 417}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 25, "privilege": "worker"}, "organization": {"id": 124, "owner": {"id": 215}, "user": {"role": "supervisor"}}}, "resource": {"id": 152, "owner": {"id": 25}, "organization": {"id": 124}, "project": {"owner": {"id": 478}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 91, "privilege": "worker"}, "organization": {"id": 143, "owner": {"id": 213}, "user": {"role": "supervisor"}}}, "resource": {"id": 150, "owner": {"id": 91}, "organization": {"id": 358}, "project": {"owner": {"id": 424}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 86, "privilege": "worker"}, "organization": {"id": 175, "owner": {"id": 215}, "user": {"role": "worker"}}}, "resource": {"id": 196, "owner": {"id": 86}, "organization": {"id": 175}, "project": {"owner": {"id": 481}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 75, "privilege": "worker"}, "organization": {"id": 164, "owner": {"id": 217}, "user": {"role": "worker"}}}, "resource": {"id": 198, "owner": {"id": 75}, "organization": {"id": 358}, "project": {"owner": {"id": 484}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 6, "privilege": "worker"}, "organization": {"id": 170, "owner": {"id": 255}, "user": {"role": null}}}, "resource": {"id": 163, "owner": {"id": 6}, "organization": {"id": 170}, "project": {"owner": {"id": 494}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 22, "privilege": "worker"}, "organization": {"id": 188, "owner": {"id": 274}, "user": {"role": null}}}, "resource": {"id": 174, "owner": {"id": 22}, "organization": {"id": 365}, "project": {"owner": {"id": 499}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 49, "privilege": "none"}, "organization": {"id": 184, "owner": {"id": 49}, "user": {"role": "owner"}}}, "resource": {"id": 122, "owner": {"id": 49}, "organization": {"id": 184}, "project": {"owner": {"id": 413}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 53, "privilege": "none"}, "organization": {"id": 177, "owner": {"id": 53}, "user": {"role": "owner"}}}, "resource": {"id": 177, "owner": {"id": 53}, "organization": {"id": 364}, "project": {"owner": {"id": 449}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 43, "privilege": "none"}, "organization": {"id": 137, "owner": {"id": 257}, "user": {"role": "maintainer"}}}, "resource": {"id": 197, "owner": {"id": 43}, "organization": {"id": 137}, "project": {"owner": {"id": 450}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 89, "privilege": "none"}, "organization": {"id": 195, "owner": {"id": 265}, "user": {"role": "maintainer"}}}, "resource": {"id": 117, "owner": {"id": 89}, "organization": {"id": 352}, "project": {"owner": {"id": 477}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 91, "privilege": "none"}, "organization": {"id": 194, "owner": {"id": 270}, "user": {"role": "supervisor"}}}, "resource": {"id": 138, "owner": {"id": 91}, "organization": {"id": 194}, "project": {"owner": {"id": 440}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 79, "privilege": "none"}, "organization": {"id": 191, "owner": {"id": 237}, "user": {"role": "supervisor"}}}, "resource": {"id": 180, "owner": {"id": 79}, "organization": {"id": 325}, "project": {"owner": {"id": 426}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 90, "privilege": "none"}, "organization": {"id": 183, "owner": {"id": 222}, "user": {"role": "worker"}}}, "resource": {"id": 187, "owner": {"id": 90}, "organization": {"id": 183}, "project": {"owner": {"id": 416}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 41, "privilege": "none"}, "organization": {"id": 190, "owner": {"id": 214}, "user": {"role": "worker"}}}, "resource": {"id": 164, "owner": {"id": 41}, "organization": {"id": 388}, "project": {"owner": {"id": 481}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 74, "privilege": "none"}, "organization": {"id": 173, "owner": {"id": 285}, "user": {"role": null}}}, "resource": {"id": 197, "owner": {"id": 74}, "organization": {"id": 173}, "project": {"owner": {"id": 461}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 68, "privilege": "none"}, "organization": {"id": 136, "owner": {"id": 254}, "user": {"role": null}}}, "resource": {"id": 190, "owner": {"id": 68}, "organization": {"id": 386}, "project": {"owner": {"id": 495}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 48, "privilege": "admin"}, "organization": {"id": 118, "owner": {"id": 48}, "user": {"role": "owner"}}}, "resource": {"id": 101, "owner": {"id": 266}, "organization": {"id": 118}, "project": {"owner": {"id": 403}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_OWNER_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 96, "privilege": "admin"}, "organization": {"id": 120, "owner": {"id": 96}, "user": {"role": "owner"}}}, "resource": {"id": 185, "owner": {"id": 207}, "organization": {"id": 325}, "project": {"owner": {"id": 434}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 88, "privilege": "admin"}, "organization": {"id": 137, "owner": {"id": 226}, "user": {"role": "maintainer"}}}, "resource": {"id": 187, "owner": {"id": 232}, "organization": {"id": 137}, "project": {"owner": {"id": 407}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 57, "privilege": "admin"}, "organization": {"id": 113, "owner": {"id": 278}, "user": {"role": "maintainer"}}}, "resource": {"id": 196, "owner": {"id": 269}, "organization": {"id": 304}, "project": {"owner": {"id": 445}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 95, "privilege": "admin"}, "organization": {"id": 182, "owner": {"id": 271}, "user": {"role": "supervisor"}}}, "resource": {"id": 186, "owner": {"id": 294}, "organization": {"id": 182}, "project": {"owner": {"id": 428}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 36, "privilege": "admin"}, "organization": {"id": 188, "owner": {"id": 285}, "user": {"role": "supervisor"}}}, "resource": {"id": 149, "owner": {"id": 230}, "organization": {"id": 365}, "project": {"owner": {"id": 489}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 87, "privilege": "admin"}, "organization": {"id": 140, "owner": {"id": 270}, "user": {"role": "worker"}}}, "resource": {"id": 106, "owner": {"id": 248}, "organization": {"id": 140}, "project": {"owner": {"id": 453}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_WORKER_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 25, "privilege": "admin"}, "organization": {"id": 179, "owner": {"id": 299}, "user": {"role": "worker"}}}, "resource": {"id": 106, "owner": {"id": 201}, "organization": {"id": 395}, "project": {"owner": {"id": 432}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 90, "privilege": "admin"}, "organization": {"id": 182, "owner": {"id": 247}, "user": {"role": null}}}, "resource": {"id": 156, "owner": {"id": 228}, "organization": {"id": 182}, "project": {"owner": {"id": 490}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_NONE_resource_project_same_org_FALSE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 77, "privilege": "admin"}, "organization": {"id": 125, "owner": {"id": 299}, "user": {"role": null}}}, "resource": {"id": 196, "owner": {"id": 275}, "organization": {"id": 369}, "project": {"owner": {"id": 494}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 22, "privilege": "business"}, "organization": {"id": 187, "owner": {"id": 22}, "user": {"role": "owner"}}}, "resource": {"id": 192, "owner": {"id": 224}, "organization": {"id": 187}, "project": {"owner": {"id": 457}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 15, "privilege": "business"}, "organization": {"id": 148, "owner": {"id": 15}, "user": {"role": "owner"}}}, "resource": {"id": 123, "owner": {"id": 222}, "organization": {"id": 394}, "project": {"owner": {"id": 465}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 16, "privilege": "business"}, "organization": {"id": 120, "owner": {"id": 274}, "user": {"role": "maintainer"}}}, "resource": {"id": 154, "owner": {"id": 235}, "organization": {"id": 120}, "project": {"owner": {"id": 433}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 90, "privilege": "business"}, "organization": {"id": 119, "owner": {"id": 205}, "user": {"role": "maintainer"}}}, "resource": {"id": 132, "owner": {"id": 200}, "organization": {"id": 342}, "project": {"owner": {"id": 459}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 99, "privilege": "business"}, "organization": {"id": 180, "owner": {"id": 278}, "user": {"role": "supervisor"}}}, "resource": {"id": 119, "owner": {"id": 241}, "organization": {"id": 180}, "project": {"owner": {"id": 406}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 70, "privilege": "business"}, "organization": {"id": 145, "owner": {"id": 209}, "user": {"role": "supervisor"}}}, "resource": {"id": 184, "owner": {"id": 238}, "organization": {"id": 362}, "project": {"owner": {"id": 473}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 23, "privilege": "business"}, "organization": {"id": 166, "owner": {"id": 208}, "user": {"role": "worker"}}}, "resource": {"id": 190, "owner": {"id": 240}, "organization": {"id": 166}, "project": {"owner": {"id": 428}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 68, "privilege": "business"}, "organization": {"id": 151, "owner": {"id": 211}, "user": {"role": "worker"}}}, "resource": {"id": 164, "owner": {"id": 220}, "organization": {"id": 353}, "project": {"owner": {"id": 469}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 42, "privilege": "business"}, "organization": {"id": 142, "owner": {"id": 246}, "user": {"role": null}}}, "resource": {"id": 144, "owner": {"id": 228}, "organization": {"id": 142}, "project": {"owner": {"id": 484}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 67, "privilege": "business"}, "organization": {"id": 160, "owner": {"id": 271}, "user": {"role": null}}}, "resource": {"id": 137, "owner": {"id": 227}, "organization": {"id": 396}, "project": {"owner": {"id": 479}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 85, "privilege": "user"}, "organization": {"id": 144, "owner": {"id": 85}, "user": {"role": "owner"}}}, "resource": {"id": 196, "owner": {"id": 283}, "organization": {"id": 144}, "project": {"owner": {"id": 414}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 31, "privilege": "user"}, "organization": {"id": 105, "owner": {"id": 31}, "user": {"role": "owner"}}}, "resource": {"id": 130, "owner": {"id": 282}, "organization": {"id": 378}, "project": {"owner": {"id": 479}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 32, "privilege": "user"}, "organization": {"id": 169, "owner": {"id": 292}, "user": {"role": "maintainer"}}}, "resource": {"id": 141, "owner": {"id": 248}, "organization": {"id": 169}, "project": {"owner": {"id": 449}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 47, "privilege": "user"}, "organization": {"id": 165, "owner": {"id": 265}, "user": {"role": "maintainer"}}}, "resource": {"id": 136, "owner": {"id": 203}, "organization": {"id": 366}, "project": {"owner": {"id": 498}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 83, "privilege": "user"}, "organization": {"id": 124, "owner": {"id": 241}, "user": {"role": "supervisor"}}}, "resource": {"id": 157, "owner": {"id": 262}, "organization": {"id": 124}, "project": {"owner": {"id": 437}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 98, "privilege": "user"}, "organization": {"id": 168, "owner": {"id": 268}, "user": {"role": "supervisor"}}}, "resource": {"id": 165, "owner": {"id": 210}, "organization": {"id": 312}, "project": {"owner": {"id": 421}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 85, "privilege": "user"}, "organization": {"id": 180, "owner": {"id": 299}, "user": {"role": "worker"}}}, "resource": {"id": 100, "owner": {"id": 280}, "organization": {"id": 180}, "project": {"owner": {"id": 426}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 26, "privilege": "user"}, "organization": {"id": 168, "owner": {"id": 289}, "user": {"role": "worker"}}}, "resource": {"id": 127, "owner": {"id": 287}, "organization": {"id": 354}, "project": {"owner": {"id": 413}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 9, "privilege": "user"}, "organization": {"id": 195, "owner": {"id": 287}, "user": {"role": null}}}, "resource": {"id": 194, "owner": {"id": 299}, "organization": {"id": 195}, "project": {"owner": {"id": 482}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 42, "privilege": "user"}, "organization": {"id": 104, "owner": {"id": 211}, "user": {"role": null}}}, "resource": {"id": 119, "owner": {"id": 203}, "organization": {"id": 358}, "project": {"owner": {"id": 491}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 71, "privilege": "worker"}, "organization": {"id": 109, "owner": {"id": 71}, "user": {"role": "owner"}}}, "resource": {"id": 109, "owner": {"id": 206}, "organization": {"id": 109}, "project": {"owner": {"id": 432}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 57, "privilege": "worker"}, "organization": {"id": 180, "owner": {"id": 57}, "user": {"role": "owner"}}}, "resource": {"id": 129, "owner": {"id": 233}, "organization": {"id": 352}, "project": {"owner": {"id": 449}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "delete", "auth": {"user": {"id": 86, "privilege": "worker"}, "organization": {"id": 115, "owner": {"id": 268}, "user": {"role": "maintainer"}}}, "resource": {"id": 155, "owner": {"id": 240}, "organization": {"id": 115}, "project": {"owner": {"id": 449}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 8, "privilege": "worker"}, "organization": {"id": 196, "owner": {"id": 295}, "user": {"role": "maintainer"}}}, "resource": {"id": 100, "owner": {"id": 282}, "organization": {"id": 392}, "project": {"owner": {"id": 479}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 45, "privilege": "worker"}, "organization": {"id": 164, "owner": {"id": 213}, "user": {"role": "supervisor"}}}, "resource": {"id": 174, "owner": {"id": 205}, "organization": {"id": 164}, "project": {"owner": {"id": 409}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 11, "privilege": "worker"}, "organization": {"id": 136, "owner": {"id": 297}, "user": {"role": "supervisor"}}}, "resource": {"id": 137, "owner": {"id": 287}, "organization": {"id": 338}, "project": {"owner": {"id": 477}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 3, "privilege": "worker"}, "organization": {"id": 160, "owner": {"id": 220}, "user": {"role": "worker"}}}, "resource": {"id": 156, "owner": {"id": 248}, "organization": {"id": 160}, "project": {"owner": {"id": 484}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 50, "privilege": "worker"}, "organization": {"id": 169, "owner": {"id": 237}, "user": {"role": "worker"}}}, "resource": {"id": 168, "owner": {"id": 228}, "organization": {"id": 317}, "project": {"owner": {"id": 495}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 93, "privilege": "worker"}, "organization": {"id": 182, "owner": {"id": 247}, "user": {"role": null}}}, "resource": {"id": 181, "owner": {"id": 218}, "organization": {"id": 182}, "project": {"owner": {"id": 486}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 15, "privilege": "worker"}, "organization": {"id": 105, "owner": {"id": 200}, "user": {"role": null}}}, "resource": {"id": 101, "owner": {"id": 270}, "organization": {"id": 371}, "project": {"owner": {"id": 418}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_OWNER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 70, "privilege": "none"}, "organization": {"id": 110, "owner": {"id": 70}, "user": {"role": "owner"}}}, "resource": {"id": 173, "owner": {"id": 276}, "organization": {"id": 110}, "project": {"owner": {"id": 468}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 44, "privilege": "none"}, "organization": {"id": 127, "owner": {"id": 44}, "user": {"role": "owner"}}}, "resource": {"id": 126, "owner": {"id": 295}, "organization": {"id": 394}, "project": {"owner": {"id": 492}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_MAINTAINER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 23, "privilege": "none"}, "organization": {"id": 128, "owner": {"id": 278}, "user": {"role": "maintainer"}}}, "resource": {"id": 182, "owner": {"id": 265}, "organization": {"id": 128}, "project": {"owner": {"id": 420}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 85, "privilege": "none"}, "organization": {"id": 172, "owner": {"id": 206}, "user": {"role": "maintainer"}}}, "resource": {"id": 132, "owner": {"id": 224}, "organization": {"id": 314}, "project": {"owner": {"id": 423}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 70, "privilege": "none"}, "organization": {"id": 109, "owner": {"id": 237}, "user": {"role": "supervisor"}}}, "resource": {"id": 196, "owner": {"id": 270}, "organization": {"id": 109}, "project": {"owner": {"id": 482}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 25, "privilege": "none"}, "organization": {"id": 192, "owner": {"id": 274}, "user": {"role": "supervisor"}}}, "resource": {"id": 185, "owner": {"id": 208}, "organization": {"id": 332}, "project": {"owner": {"id": 412}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 85, "privilege": "none"}, "organization": {"id": 183, "owner": {"id": 230}, "user": {"role": "worker"}}}, "resource": {"id": 162, "owner": {"id": 242}, "organization": {"id": 183}, "project": {"owner": {"id": 416}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 24, "privilege": "none"}, "organization": {"id": 140, "owner": {"id": 262}, "user": {"role": "worker"}}}, "resource": {"id": 112, "owner": {"id": 236}, "organization": {"id": 379}, "project": {"owner": {"id": 408}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 39, "privilege": "none"}, "organization": {"id": 188, "owner": {"id": 274}, "user": {"role": null}}}, "resource": {"id": 157, "owner": {"id": 242}, "organization": {"id": 188}, "project": {"owner": {"id": 484}}}} +} + +test_scope_DELETE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "delete", "auth": {"user": {"id": 40, "privilege": "none"}, "organization": {"id": 154, "owner": {"id": 221}, "user": {"role": null}}}, "resource": {"id": 119, "owner": {"id": 272}, "organization": {"id": 372}, "project": {"owner": {"id": 446}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 41, "privilege": "admin"}, "organization": null}, "resource": {"id": 171, "owner": {"id": 292}, "organization": {"id": 305}, "project": {"owner": {"id": 41}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 21, "privilege": "business"}, "organization": null}, "resource": {"id": 188, "owner": {"id": 275}, "organization": {"id": 313}, "project": {"owner": {"id": 21}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 17, "privilege": "user"}, "organization": null}, "resource": {"id": 162, "owner": {"id": 263}, "organization": {"id": 329}, "project": {"owner": {"id": 17}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 55, "privilege": "worker"}, "organization": null}, "resource": {"id": 169, "owner": {"id": 202}, "organization": {"id": 397}, "project": {"owner": {"id": 55}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_PROJECT_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 33, "privilege": "none"}, "organization": null}, "resource": {"id": 120, "owner": {"id": 264}, "organization": {"id": 307}, "project": {"owner": {"id": 33}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 39, "privilege": "admin"}, "organization": null}, "resource": {"id": 175, "owner": {"id": 39}, "organization": {"id": 388}, "project": {"owner": {"id": 408}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 46, "privilege": "business"}, "organization": null}, "resource": {"id": 143, "owner": {"id": 46}, "organization": {"id": 378}, "project": {"owner": {"id": 438}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 53, "privilege": "user"}, "organization": null}, "resource": {"id": 184, "owner": {"id": 53}, "organization": {"id": 340}, "project": {"owner": {"id": 425}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 38, "privilege": "worker"}, "organization": null}, "resource": {"id": 119, "owner": {"id": 38}, "organization": {"id": 387}, "project": {"owner": {"id": 497}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 82, "privilege": "none"}, "organization": null}, "resource": {"id": 135, "owner": {"id": 82}, "organization": {"id": 370}, "project": {"owner": {"id": 422}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_NONE_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 49, "privilege": "admin"}, "organization": null}, "resource": {"id": 140, "owner": {"id": 242}, "organization": {"id": 374}, "project": {"owner": {"id": 494}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_NONE_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 88, "privilege": "business"}, "organization": null}, "resource": {"id": 104, "owner": {"id": 294}, "organization": {"id": 384}, "project": {"owner": {"id": 406}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_NONE_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 91, "privilege": "user"}, "organization": null}, "resource": {"id": 160, "owner": {"id": 294}, "organization": {"id": 352}, "project": {"owner": {"id": 442}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_NONE_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 83, "privilege": "worker"}, "organization": null}, "resource": {"id": 131, "owner": {"id": 266}, "organization": {"id": 391}, "project": {"owner": {"id": 498}}}} +} + +test_scope_UPDATE_context_SANDBOX_ownership_NONE_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 79, "privilege": "none"}, "organization": null}, "resource": {"id": 189, "owner": {"id": 221}, "organization": {"id": 306}, "project": {"owner": {"id": 451}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 15, "privilege": "admin"}, "organization": {"id": 101, "owner": {"id": 15}, "user": {"role": "owner"}}}, "resource": {"id": 111, "owner": {"id": 247}, "organization": {"id": 101}, "project": {"owner": {"id": 15}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 86, "privilege": "admin"}, "organization": {"id": 163, "owner": {"id": 86}, "user": {"role": "owner"}}}, "resource": {"id": 167, "owner": {"id": 255}, "organization": {"id": 398}, "project": {"owner": {"id": 86}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 34, "privilege": "admin"}, "organization": {"id": 136, "owner": {"id": 295}, "user": {"role": "maintainer"}}}, "resource": {"id": 128, "owner": {"id": 291}, "organization": {"id": 136}, "project": {"owner": {"id": 34}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 26, "privilege": "admin"}, "organization": {"id": 176, "owner": {"id": 227}, "user": {"role": "maintainer"}}}, "resource": {"id": 163, "owner": {"id": 248}, "organization": {"id": 316}, "project": {"owner": {"id": 26}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 53, "privilege": "admin"}, "organization": {"id": 123, "owner": {"id": 252}, "user": {"role": "supervisor"}}}, "resource": {"id": 136, "owner": {"id": 293}, "organization": {"id": 123}, "project": {"owner": {"id": 53}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 30, "privilege": "admin"}, "organization": {"id": 179, "owner": {"id": 257}, "user": {"role": "supervisor"}}}, "resource": {"id": 118, "owner": {"id": 203}, "organization": {"id": 337}, "project": {"owner": {"id": 30}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 82, "privilege": "admin"}, "organization": {"id": 158, "owner": {"id": 210}, "user": {"role": "worker"}}}, "resource": {"id": 131, "owner": {"id": 247}, "organization": {"id": 158}, "project": {"owner": {"id": 82}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 88, "privilege": "admin"}, "organization": {"id": 150, "owner": {"id": 287}, "user": {"role": "worker"}}}, "resource": {"id": 101, "owner": {"id": 260}, "organization": {"id": 317}, "project": {"owner": {"id": 88}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 54, "privilege": "admin"}, "organization": {"id": 194, "owner": {"id": 276}, "user": {"role": null}}}, "resource": {"id": 127, "owner": {"id": 291}, "organization": {"id": 194}, "project": {"owner": {"id": 54}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 7, "privilege": "admin"}, "organization": {"id": 106, "owner": {"id": 242}, "user": {"role": null}}}, "resource": {"id": 132, "owner": {"id": 209}, "organization": {"id": 312}, "project": {"owner": {"id": 7}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 85, "privilege": "business"}, "organization": {"id": 176, "owner": {"id": 85}, "user": {"role": "owner"}}}, "resource": {"id": 130, "owner": {"id": 260}, "organization": {"id": 176}, "project": {"owner": {"id": 85}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 64, "privilege": "business"}, "organization": {"id": 179, "owner": {"id": 64}, "user": {"role": "owner"}}}, "resource": {"id": 195, "owner": {"id": 205}, "organization": {"id": 300}, "project": {"owner": {"id": 64}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 16, "privilege": "business"}, "organization": {"id": 171, "owner": {"id": 205}, "user": {"role": "maintainer"}}}, "resource": {"id": 138, "owner": {"id": 250}, "organization": {"id": 171}, "project": {"owner": {"id": 16}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 64, "privilege": "business"}, "organization": {"id": 163, "owner": {"id": 243}, "user": {"role": "maintainer"}}}, "resource": {"id": 174, "owner": {"id": 222}, "organization": {"id": 333}, "project": {"owner": {"id": 64}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 90, "privilege": "business"}, "organization": {"id": 167, "owner": {"id": 255}, "user": {"role": "supervisor"}}}, "resource": {"id": 159, "owner": {"id": 216}, "organization": {"id": 167}, "project": {"owner": {"id": 90}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 5, "privilege": "business"}, "organization": {"id": 116, "owner": {"id": 263}, "user": {"role": "supervisor"}}}, "resource": {"id": 118, "owner": {"id": 233}, "organization": {"id": 314}, "project": {"owner": {"id": 5}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 36, "privilege": "business"}, "organization": {"id": 133, "owner": {"id": 278}, "user": {"role": "worker"}}}, "resource": {"id": 135, "owner": {"id": 295}, "organization": {"id": 133}, "project": {"owner": {"id": 36}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 55, "privilege": "business"}, "organization": {"id": 121, "owner": {"id": 240}, "user": {"role": "worker"}}}, "resource": {"id": 197, "owner": {"id": 233}, "organization": {"id": 366}, "project": {"owner": {"id": 55}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 71, "privilege": "business"}, "organization": {"id": 184, "owner": {"id": 281}, "user": {"role": null}}}, "resource": {"id": 194, "owner": {"id": 262}, "organization": {"id": 184}, "project": {"owner": {"id": 71}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 6, "privilege": "business"}, "organization": {"id": 129, "owner": {"id": 223}, "user": {"role": null}}}, "resource": {"id": 156, "owner": {"id": 206}, "organization": {"id": 324}, "project": {"owner": {"id": 6}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 90, "privilege": "user"}, "organization": {"id": 159, "owner": {"id": 90}, "user": {"role": "owner"}}}, "resource": {"id": 104, "owner": {"id": 260}, "organization": {"id": 159}, "project": {"owner": {"id": 90}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 45, "privilege": "user"}, "organization": {"id": 190, "owner": {"id": 45}, "user": {"role": "owner"}}}, "resource": {"id": 114, "owner": {"id": 263}, "organization": {"id": 305}, "project": {"owner": {"id": 45}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 92, "privilege": "user"}, "organization": {"id": 118, "owner": {"id": 215}, "user": {"role": "maintainer"}}}, "resource": {"id": 162, "owner": {"id": 258}, "organization": {"id": 118}, "project": {"owner": {"id": 92}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 23, "privilege": "user"}, "organization": {"id": 121, "owner": {"id": 295}, "user": {"role": "maintainer"}}}, "resource": {"id": 144, "owner": {"id": 211}, "organization": {"id": 326}, "project": {"owner": {"id": 23}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 74, "privilege": "user"}, "organization": {"id": 127, "owner": {"id": 274}, "user": {"role": "supervisor"}}}, "resource": {"id": 165, "owner": {"id": 234}, "organization": {"id": 127}, "project": {"owner": {"id": 74}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 23, "privilege": "user"}, "organization": {"id": 153, "owner": {"id": 209}, "user": {"role": "supervisor"}}}, "resource": {"id": 100, "owner": {"id": 267}, "organization": {"id": 398}, "project": {"owner": {"id": 23}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 12, "privilege": "user"}, "organization": {"id": 142, "owner": {"id": 203}, "user": {"role": "worker"}}}, "resource": {"id": 131, "owner": {"id": 298}, "organization": {"id": 142}, "project": {"owner": {"id": 12}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 50, "privilege": "user"}, "organization": {"id": 147, "owner": {"id": 275}, "user": {"role": "worker"}}}, "resource": {"id": 136, "owner": {"id": 287}, "organization": {"id": 387}, "project": {"owner": {"id": 50}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 10, "privilege": "user"}, "organization": {"id": 158, "owner": {"id": 290}, "user": {"role": null}}}, "resource": {"id": 113, "owner": {"id": 241}, "organization": {"id": 158}, "project": {"owner": {"id": 10}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_USER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 34, "privilege": "user"}, "organization": {"id": 176, "owner": {"id": 284}, "user": {"role": null}}}, "resource": {"id": 184, "owner": {"id": 261}, "organization": {"id": 358}, "project": {"owner": {"id": 34}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 79, "privilege": "worker"}, "organization": {"id": 157, "owner": {"id": 79}, "user": {"role": "owner"}}}, "resource": {"id": 100, "owner": {"id": 242}, "organization": {"id": 157}, "project": {"owner": {"id": 79}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 27, "privilege": "worker"}, "organization": {"id": 106, "owner": {"id": 27}, "user": {"role": "owner"}}}, "resource": {"id": 193, "owner": {"id": 293}, "organization": {"id": 372}, "project": {"owner": {"id": 27}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 9, "privilege": "worker"}, "organization": {"id": 170, "owner": {"id": 277}, "user": {"role": "maintainer"}}}, "resource": {"id": 136, "owner": {"id": 241}, "organization": {"id": 170}, "project": {"owner": {"id": 9}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 22, "privilege": "worker"}, "organization": {"id": 130, "owner": {"id": 298}, "user": {"role": "maintainer"}}}, "resource": {"id": 148, "owner": {"id": 211}, "organization": {"id": 351}, "project": {"owner": {"id": 22}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 20, "privilege": "worker"}, "organization": {"id": 149, "owner": {"id": 299}, "user": {"role": "supervisor"}}}, "resource": {"id": 151, "owner": {"id": 211}, "organization": {"id": 149}, "project": {"owner": {"id": 20}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 83, "privilege": "worker"}, "organization": {"id": 119, "owner": {"id": 215}, "user": {"role": "supervisor"}}}, "resource": {"id": 151, "owner": {"id": 236}, "organization": {"id": 382}, "project": {"owner": {"id": 83}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 39, "privilege": "worker"}, "organization": {"id": 187, "owner": {"id": 277}, "user": {"role": "worker"}}}, "resource": {"id": 138, "owner": {"id": 262}, "organization": {"id": 187}, "project": {"owner": {"id": 39}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 47, "privilege": "worker"}, "organization": {"id": 165, "owner": {"id": 225}, "user": {"role": "worker"}}}, "resource": {"id": 116, "owner": {"id": 245}, "organization": {"id": 385}, "project": {"owner": {"id": 47}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 34, "privilege": "worker"}, "organization": {"id": 172, "owner": {"id": 276}, "user": {"role": null}}}, "resource": {"id": 162, "owner": {"id": 257}, "organization": {"id": 172}, "project": {"owner": {"id": 34}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 47, "privilege": "worker"}, "organization": {"id": 178, "owner": {"id": 223}, "user": {"role": null}}}, "resource": {"id": 154, "owner": {"id": 211}, "organization": {"id": 396}, "project": {"owner": {"id": 47}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 5, "privilege": "none"}, "organization": {"id": 165, "owner": {"id": 5}, "user": {"role": "owner"}}}, "resource": {"id": 155, "owner": {"id": 280}, "organization": {"id": 165}, "project": {"owner": {"id": 5}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 87, "privilege": "none"}, "organization": {"id": 191, "owner": {"id": 87}, "user": {"role": "owner"}}}, "resource": {"id": 159, "owner": {"id": 286}, "organization": {"id": 328}, "project": {"owner": {"id": 87}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 67, "privilege": "none"}, "organization": {"id": 110, "owner": {"id": 237}, "user": {"role": "maintainer"}}}, "resource": {"id": 163, "owner": {"id": 267}, "organization": {"id": 110}, "project": {"owner": {"id": 67}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 38, "privilege": "none"}, "organization": {"id": 119, "owner": {"id": 216}, "user": {"role": "maintainer"}}}, "resource": {"id": 159, "owner": {"id": 255}, "organization": {"id": 310}, "project": {"owner": {"id": 38}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 54, "privilege": "none"}, "organization": {"id": 137, "owner": {"id": 272}, "user": {"role": "supervisor"}}}, "resource": {"id": 187, "owner": {"id": 238}, "organization": {"id": 137}, "project": {"owner": {"id": 54}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 65, "privilege": "none"}, "organization": {"id": 113, "owner": {"id": 265}, "user": {"role": "supervisor"}}}, "resource": {"id": 106, "owner": {"id": 270}, "organization": {"id": 309}, "project": {"owner": {"id": 65}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 84, "privilege": "none"}, "organization": {"id": 192, "owner": {"id": 272}, "user": {"role": "worker"}}}, "resource": {"id": 154, "owner": {"id": 252}, "organization": {"id": 192}, "project": {"owner": {"id": 84}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 10, "privilege": "none"}, "organization": {"id": 198, "owner": {"id": 264}, "user": {"role": "worker"}}}, "resource": {"id": 132, "owner": {"id": 245}, "organization": {"id": 332}, "project": {"owner": {"id": 10}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 59, "privilege": "none"}, "organization": {"id": 122, "owner": {"id": 223}, "user": {"role": null}}}, "resource": {"id": 102, "owner": {"id": 214}, "organization": {"id": 122}, "project": {"owner": {"id": 59}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_PROJECT_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 90, "privilege": "none"}, "organization": {"id": 138, "owner": {"id": 240}, "user": {"role": null}}}, "resource": {"id": 150, "owner": {"id": 205}, "organization": {"id": 310}, "project": {"owner": {"id": 90}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 85, "privilege": "admin"}, "organization": {"id": 157, "owner": {"id": 85}, "user": {"role": "owner"}}}, "resource": {"id": 175, "owner": {"id": 85}, "organization": {"id": 157}, "project": {"owner": {"id": 496}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_OWNER_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 23, "privilege": "admin"}, "organization": {"id": 110, "owner": {"id": 23}, "user": {"role": "owner"}}}, "resource": {"id": 133, "owner": {"id": 23}, "organization": {"id": 323}, "project": {"owner": {"id": 427}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 55, "privilege": "admin"}, "organization": {"id": 168, "owner": {"id": 202}, "user": {"role": "maintainer"}}}, "resource": {"id": 177, "owner": {"id": 55}, "organization": {"id": 168}, "project": {"owner": {"id": 413}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 21, "privilege": "admin"}, "organization": {"id": 183, "owner": {"id": 261}, "user": {"role": "maintainer"}}}, "resource": {"id": 100, "owner": {"id": 21}, "organization": {"id": 314}, "project": {"owner": {"id": 401}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 95, "privilege": "admin"}, "organization": {"id": 177, "owner": {"id": 292}, "user": {"role": "supervisor"}}}, "resource": {"id": 158, "owner": {"id": 95}, "organization": {"id": 177}, "project": {"owner": {"id": 484}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 46, "privilege": "admin"}, "organization": {"id": 188, "owner": {"id": 286}, "user": {"role": "supervisor"}}}, "resource": {"id": 162, "owner": {"id": 46}, "organization": {"id": 379}, "project": {"owner": {"id": 478}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 11, "privilege": "admin"}, "organization": {"id": 116, "owner": {"id": 217}, "user": {"role": "worker"}}}, "resource": {"id": 188, "owner": {"id": 11}, "organization": {"id": 116}, "project": {"owner": {"id": 438}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_WORKER_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 89, "privilege": "admin"}, "organization": {"id": 124, "owner": {"id": 203}, "user": {"role": "worker"}}}, "resource": {"id": 101, "owner": {"id": 89}, "organization": {"id": 314}, "project": {"owner": {"id": 402}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 83, "privilege": "admin"}, "organization": {"id": 130, "owner": {"id": 255}, "user": {"role": null}}}, "resource": {"id": 126, "owner": {"id": 83}, "organization": {"id": 130}, "project": {"owner": {"id": 418}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_ADMIN_membership_NONE_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 38, "privilege": "admin"}, "organization": {"id": 198, "owner": {"id": 259}, "user": {"role": null}}}, "resource": {"id": 180, "owner": {"id": 38}, "organization": {"id": 324}, "project": {"owner": {"id": 411}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 14, "privilege": "business"}, "organization": {"id": 121, "owner": {"id": 14}, "user": {"role": "owner"}}}, "resource": {"id": 118, "owner": {"id": 14}, "organization": {"id": 121}, "project": {"owner": {"id": 402}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 60, "privilege": "business"}, "organization": {"id": 102, "owner": {"id": 60}, "user": {"role": "owner"}}}, "resource": {"id": 194, "owner": {"id": 60}, "organization": {"id": 356}, "project": {"owner": {"id": 462}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 72, "privilege": "business"}, "organization": {"id": 150, "owner": {"id": 206}, "user": {"role": "maintainer"}}}, "resource": {"id": 174, "owner": {"id": 72}, "organization": {"id": 150}, "project": {"owner": {"id": 437}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 91, "privilege": "business"}, "organization": {"id": 155, "owner": {"id": 253}, "user": {"role": "maintainer"}}}, "resource": {"id": 142, "owner": {"id": 91}, "organization": {"id": 313}, "project": {"owner": {"id": 480}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 93, "privilege": "business"}, "organization": {"id": 170, "owner": {"id": 237}, "user": {"role": "supervisor"}}}, "resource": {"id": 163, "owner": {"id": 93}, "organization": {"id": 170}, "project": {"owner": {"id": 476}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 65, "privilege": "business"}, "organization": {"id": 175, "owner": {"id": 292}, "user": {"role": "supervisor"}}}, "resource": {"id": 104, "owner": {"id": 65}, "organization": {"id": 351}, "project": {"owner": {"id": 462}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 91, "privilege": "business"}, "organization": {"id": 107, "owner": {"id": 267}, "user": {"role": "worker"}}}, "resource": {"id": 178, "owner": {"id": 91}, "organization": {"id": 107}, "project": {"owner": {"id": 429}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 70, "privilege": "business"}, "organization": {"id": 108, "owner": {"id": 234}, "user": {"role": "worker"}}}, "resource": {"id": 102, "owner": {"id": 70}, "organization": {"id": 373}, "project": {"owner": {"id": 440}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 63, "privilege": "business"}, "organization": {"id": 179, "owner": {"id": 282}, "user": {"role": null}}}, "resource": {"id": 182, "owner": {"id": 63}, "organization": {"id": 179}, "project": {"owner": {"id": 414}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_BUSINESS_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 2, "privilege": "business"}, "organization": {"id": 120, "owner": {"id": 236}, "user": {"role": null}}}, "resource": {"id": 144, "owner": {"id": 2}, "organization": {"id": 389}, "project": {"owner": {"id": 455}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 56, "privilege": "user"}, "organization": {"id": 129, "owner": {"id": 56}, "user": {"role": "owner"}}}, "resource": {"id": 192, "owner": {"id": 56}, "organization": {"id": 129}, "project": {"owner": {"id": 444}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 77, "privilege": "user"}, "organization": {"id": 191, "owner": {"id": 77}, "user": {"role": "owner"}}}, "resource": {"id": 128, "owner": {"id": 77}, "organization": {"id": 387}, "project": {"owner": {"id": 443}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 91, "privilege": "user"}, "organization": {"id": 149, "owner": {"id": 253}, "user": {"role": "maintainer"}}}, "resource": {"id": 159, "owner": {"id": 91}, "organization": {"id": 149}, "project": {"owner": {"id": 492}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 60, "privilege": "user"}, "organization": {"id": 120, "owner": {"id": 279}, "user": {"role": "maintainer"}}}, "resource": {"id": 186, "owner": {"id": 60}, "organization": {"id": 394}, "project": {"owner": {"id": 446}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 11, "privilege": "user"}, "organization": {"id": 185, "owner": {"id": 261}, "user": {"role": "supervisor"}}}, "resource": {"id": 126, "owner": {"id": 11}, "organization": {"id": 185}, "project": {"owner": {"id": 495}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 77, "privilege": "user"}, "organization": {"id": 162, "owner": {"id": 212}, "user": {"role": "supervisor"}}}, "resource": {"id": 116, "owner": {"id": 77}, "organization": {"id": 325}, "project": {"owner": {"id": 420}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 26, "privilege": "user"}, "organization": {"id": 171, "owner": {"id": 206}, "user": {"role": "worker"}}}, "resource": {"id": 153, "owner": {"id": 26}, "organization": {"id": 171}, "project": {"owner": {"id": 430}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 86, "privilege": "user"}, "organization": {"id": 120, "owner": {"id": 222}, "user": {"role": "worker"}}}, "resource": {"id": 102, "owner": {"id": 86}, "organization": {"id": 333}, "project": {"owner": {"id": 445}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 55, "privilege": "user"}, "organization": {"id": 114, "owner": {"id": 279}, "user": {"role": null}}}, "resource": {"id": 164, "owner": {"id": 55}, "organization": {"id": 114}, "project": {"owner": {"id": 443}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_USER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 62, "privilege": "user"}, "organization": {"id": 133, "owner": {"id": 243}, "user": {"role": null}}}, "resource": {"id": 129, "owner": {"id": 62}, "organization": {"id": 371}, "project": {"owner": {"id": 428}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 71, "privilege": "worker"}, "organization": {"id": 134, "owner": {"id": 71}, "user": {"role": "owner"}}}, "resource": {"id": 145, "owner": {"id": 71}, "organization": {"id": 134}, "project": {"owner": {"id": 498}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 13, "privilege": "worker"}, "organization": {"id": 190, "owner": {"id": 13}, "user": {"role": "owner"}}}, "resource": {"id": 188, "owner": {"id": 13}, "organization": {"id": 377}, "project": {"owner": {"id": 485}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 13, "privilege": "worker"}, "organization": {"id": 178, "owner": {"id": 278}, "user": {"role": "maintainer"}}}, "resource": {"id": 156, "owner": {"id": 13}, "organization": {"id": 178}, "project": {"owner": {"id": 414}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 13, "privilege": "worker"}, "organization": {"id": 121, "owner": {"id": 280}, "user": {"role": "maintainer"}}}, "resource": {"id": 113, "owner": {"id": 13}, "organization": {"id": 305}, "project": {"owner": {"id": 464}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 93, "privilege": "worker"}, "organization": {"id": 137, "owner": {"id": 290}, "user": {"role": "supervisor"}}}, "resource": {"id": 115, "owner": {"id": 93}, "organization": {"id": 137}, "project": {"owner": {"id": 420}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 93, "privilege": "worker"}, "organization": {"id": 112, "owner": {"id": 219}, "user": {"role": "supervisor"}}}, "resource": {"id": 156, "owner": {"id": 93}, "organization": {"id": 364}, "project": {"owner": {"id": 463}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 15, "privilege": "worker"}, "organization": {"id": 125, "owner": {"id": 256}, "user": {"role": "worker"}}}, "resource": {"id": 182, "owner": {"id": 15}, "organization": {"id": 125}, "project": {"owner": {"id": 417}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 31, "privilege": "worker"}, "organization": {"id": 160, "owner": {"id": 243}, "user": {"role": "worker"}}}, "resource": {"id": 153, "owner": {"id": 31}, "organization": {"id": 398}, "project": {"owner": {"id": 400}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 34, "privilege": "worker"}, "organization": {"id": 147, "owner": {"id": 291}, "user": {"role": null}}}, "resource": {"id": 136, "owner": {"id": 34}, "organization": {"id": 147}, "project": {"owner": {"id": 410}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_WORKER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 85, "privilege": "worker"}, "organization": {"id": 131, "owner": {"id": 219}, "user": {"role": null}}}, "resource": {"id": 196, "owner": {"id": 85}, "organization": {"id": 346}, "project": {"owner": {"id": 480}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 29, "privilege": "none"}, "organization": {"id": 117, "owner": {"id": 29}, "user": {"role": "owner"}}}, "resource": {"id": 196, "owner": {"id": 29}, "organization": {"id": 117}, "project": {"owner": {"id": 453}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 87, "privilege": "none"}, "organization": {"id": 146, "owner": {"id": 87}, "user": {"role": "owner"}}}, "resource": {"id": 152, "owner": {"id": 87}, "organization": {"id": 358}, "project": {"owner": {"id": 466}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 91, "privilege": "none"}, "organization": {"id": 166, "owner": {"id": 208}, "user": {"role": "maintainer"}}}, "resource": {"id": 156, "owner": {"id": 91}, "organization": {"id": 166}, "project": {"owner": {"id": 401}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 98, "privilege": "none"}, "organization": {"id": 121, "owner": {"id": 255}, "user": {"role": "maintainer"}}}, "resource": {"id": 110, "owner": {"id": 98}, "organization": {"id": 340}, "project": {"owner": {"id": 451}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 84, "privilege": "none"}, "organization": {"id": 182, "owner": {"id": 219}, "user": {"role": "supervisor"}}}, "resource": {"id": 107, "owner": {"id": 84}, "organization": {"id": 182}, "project": {"owner": {"id": 404}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 84, "privilege": "none"}, "organization": {"id": 166, "owner": {"id": 237}, "user": {"role": "supervisor"}}}, "resource": {"id": 173, "owner": {"id": 84}, "organization": {"id": 380}, "project": {"owner": {"id": 491}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 70, "privilege": "none"}, "organization": {"id": 138, "owner": {"id": 292}, "user": {"role": "worker"}}}, "resource": {"id": 153, "owner": {"id": 70}, "organization": {"id": 138}, "project": {"owner": {"id": 489}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 35, "privilege": "none"}, "organization": {"id": 167, "owner": {"id": 278}, "user": {"role": "worker"}}}, "resource": {"id": 195, "owner": {"id": 35}, "organization": {"id": 344}, "project": {"owner": {"id": 401}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 82, "privilege": "none"}, "organization": {"id": 126, "owner": {"id": 269}, "user": {"role": null}}}, "resource": {"id": 118, "owner": {"id": 82}, "organization": {"id": 126}, "project": {"owner": {"id": 457}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_OWNER_privilege_NONE_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 89, "privilege": "none"}, "organization": {"id": 144, "owner": {"id": 283}, "user": {"role": null}}}, "resource": {"id": 179, "owner": {"id": 89}, "organization": {"id": 383}, "project": {"owner": {"id": 456}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 10, "privilege": "admin"}, "organization": {"id": 104, "owner": {"id": 10}, "user": {"role": "owner"}}}, "resource": {"id": 151, "owner": {"id": 299}, "organization": {"id": 104}, "project": {"owner": {"id": 438}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_OWNER_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 94, "privilege": "admin"}, "organization": {"id": 116, "owner": {"id": 94}, "user": {"role": "owner"}}}, "resource": {"id": 142, "owner": {"id": 249}, "organization": {"id": 392}, "project": {"owner": {"id": 492}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 65, "privilege": "admin"}, "organization": {"id": 170, "owner": {"id": 239}, "user": {"role": "maintainer"}}}, "resource": {"id": 169, "owner": {"id": 236}, "organization": {"id": 170}, "project": {"owner": {"id": 411}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_MAINTAINER_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 20, "privilege": "admin"}, "organization": {"id": 102, "owner": {"id": 203}, "user": {"role": "maintainer"}}}, "resource": {"id": 166, "owner": {"id": 243}, "organization": {"id": 396}, "project": {"owner": {"id": 481}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 99, "privilege": "admin"}, "organization": {"id": 138, "owner": {"id": 292}, "user": {"role": "supervisor"}}}, "resource": {"id": 169, "owner": {"id": 275}, "organization": {"id": 138}, "project": {"owner": {"id": 430}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_SUPERVISOR_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 7, "privilege": "admin"}, "organization": {"id": 147, "owner": {"id": 233}, "user": {"role": "supervisor"}}}, "resource": {"id": 154, "owner": {"id": 264}, "organization": {"id": 314}, "project": {"owner": {"id": 493}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_WORKER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 6, "privilege": "admin"}, "organization": {"id": 196, "owner": {"id": 283}, "user": {"role": "worker"}}}, "resource": {"id": 180, "owner": {"id": 264}, "organization": {"id": 196}, "project": {"owner": {"id": 415}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_WORKER_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 94, "privilege": "admin"}, "organization": {"id": 100, "owner": {"id": 258}, "user": {"role": "worker"}}}, "resource": {"id": 115, "owner": {"id": 257}, "organization": {"id": 338}, "project": {"owner": {"id": 473}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_NONE_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 72, "privilege": "admin"}, "organization": {"id": 191, "owner": {"id": 280}, "user": {"role": null}}}, "resource": {"id": 143, "owner": {"id": 230}, "organization": {"id": 191}, "project": {"owner": {"id": 474}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_NONE_resource_project_same_org_FALSE { + allow with input as {"scope": "update", "auth": {"user": {"id": 44, "privilege": "admin"}, "organization": {"id": 138, "owner": {"id": 236}, "user": {"role": null}}}, "resource": {"id": 185, "owner": {"id": 268}, "organization": {"id": 376}, "project": {"owner": {"id": 429}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 17, "privilege": "business"}, "organization": {"id": 191, "owner": {"id": 17}, "user": {"role": "owner"}}}, "resource": {"id": 185, "owner": {"id": 291}, "organization": {"id": 191}, "project": {"owner": {"id": 486}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 41, "privilege": "business"}, "organization": {"id": 166, "owner": {"id": 41}, "user": {"role": "owner"}}}, "resource": {"id": 166, "owner": {"id": 294}, "organization": {"id": 331}, "project": {"owner": {"id": 413}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 12, "privilege": "business"}, "organization": {"id": 118, "owner": {"id": 291}, "user": {"role": "maintainer"}}}, "resource": {"id": 124, "owner": {"id": 299}, "organization": {"id": 118}, "project": {"owner": {"id": 458}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 7, "privilege": "business"}, "organization": {"id": 154, "owner": {"id": 216}, "user": {"role": "maintainer"}}}, "resource": {"id": 154, "owner": {"id": 252}, "organization": {"id": 310}, "project": {"owner": {"id": 411}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 47, "privilege": "business"}, "organization": {"id": 186, "owner": {"id": 267}, "user": {"role": "supervisor"}}}, "resource": {"id": 160, "owner": {"id": 227}, "organization": {"id": 186}, "project": {"owner": {"id": 409}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 99, "privilege": "business"}, "organization": {"id": 150, "owner": {"id": 233}, "user": {"role": "supervisor"}}}, "resource": {"id": 151, "owner": {"id": 275}, "organization": {"id": 371}, "project": {"owner": {"id": 466}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 56, "privilege": "business"}, "organization": {"id": 188, "owner": {"id": 262}, "user": {"role": "worker"}}}, "resource": {"id": 174, "owner": {"id": 263}, "organization": {"id": 188}, "project": {"owner": {"id": 454}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 64, "privilege": "business"}, "organization": {"id": 116, "owner": {"id": 262}, "user": {"role": "worker"}}}, "resource": {"id": 193, "owner": {"id": 253}, "organization": {"id": 320}, "project": {"owner": {"id": 418}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 65, "privilege": "business"}, "organization": {"id": 167, "owner": {"id": 215}, "user": {"role": null}}}, "resource": {"id": 191, "owner": {"id": 258}, "organization": {"id": 167}, "project": {"owner": {"id": 490}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 4, "privilege": "business"}, "organization": {"id": 195, "owner": {"id": 220}, "user": {"role": null}}}, "resource": {"id": 188, "owner": {"id": 227}, "organization": {"id": 358}, "project": {"owner": {"id": 460}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 52, "privilege": "user"}, "organization": {"id": 114, "owner": {"id": 52}, "user": {"role": "owner"}}}, "resource": {"id": 171, "owner": {"id": 247}, "organization": {"id": 114}, "project": {"owner": {"id": 477}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 66, "privilege": "user"}, "organization": {"id": 119, "owner": {"id": 66}, "user": {"role": "owner"}}}, "resource": {"id": 147, "owner": {"id": 273}, "organization": {"id": 337}, "project": {"owner": {"id": 405}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 37, "privilege": "user"}, "organization": {"id": 162, "owner": {"id": 203}, "user": {"role": "maintainer"}}}, "resource": {"id": 147, "owner": {"id": 239}, "organization": {"id": 162}, "project": {"owner": {"id": 438}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 6, "privilege": "user"}, "organization": {"id": 139, "owner": {"id": 236}, "user": {"role": "maintainer"}}}, "resource": {"id": 118, "owner": {"id": 218}, "organization": {"id": 375}, "project": {"owner": {"id": 453}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 0, "privilege": "user"}, "organization": {"id": 115, "owner": {"id": 270}, "user": {"role": "supervisor"}}}, "resource": {"id": 127, "owner": {"id": 223}, "organization": {"id": 115}, "project": {"owner": {"id": 462}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 61, "privilege": "user"}, "organization": {"id": 173, "owner": {"id": 239}, "user": {"role": "supervisor"}}}, "resource": {"id": 144, "owner": {"id": 208}, "organization": {"id": 305}, "project": {"owner": {"id": 453}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 60, "privilege": "user"}, "organization": {"id": 192, "owner": {"id": 277}, "user": {"role": "worker"}}}, "resource": {"id": 172, "owner": {"id": 257}, "organization": {"id": 192}, "project": {"owner": {"id": 448}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 81, "privilege": "user"}, "organization": {"id": 158, "owner": {"id": 297}, "user": {"role": "worker"}}}, "resource": {"id": 116, "owner": {"id": 297}, "organization": {"id": 317}, "project": {"owner": {"id": 464}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 93, "privilege": "user"}, "organization": {"id": 141, "owner": {"id": 211}, "user": {"role": null}}}, "resource": {"id": 157, "owner": {"id": 285}, "organization": {"id": 141}, "project": {"owner": {"id": 419}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 49, "privilege": "user"}, "organization": {"id": 129, "owner": {"id": 250}, "user": {"role": null}}}, "resource": {"id": 179, "owner": {"id": 285}, "organization": {"id": 379}, "project": {"owner": {"id": 452}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_OWNER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 56, "privilege": "worker"}, "organization": {"id": 194, "owner": {"id": 56}, "user": {"role": "owner"}}}, "resource": {"id": 143, "owner": {"id": 269}, "organization": {"id": 194}, "project": {"owner": {"id": 485}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 3, "privilege": "worker"}, "organization": {"id": 173, "owner": {"id": 3}, "user": {"role": "owner"}}}, "resource": {"id": 182, "owner": {"id": 211}, "organization": {"id": 321}, "project": {"owner": {"id": 490}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_TRUE { + allow with input as {"scope": "update", "auth": {"user": {"id": 82, "privilege": "worker"}, "organization": {"id": 115, "owner": {"id": 282}, "user": {"role": "maintainer"}}}, "resource": {"id": 198, "owner": {"id": 290}, "organization": {"id": 115}, "project": {"owner": {"id": 471}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 69, "privilege": "worker"}, "organization": {"id": 173, "owner": {"id": 296}, "user": {"role": "maintainer"}}}, "resource": {"id": 171, "owner": {"id": 210}, "organization": {"id": 324}, "project": {"owner": {"id": 479}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 96, "privilege": "worker"}, "organization": {"id": 160, "owner": {"id": 264}, "user": {"role": "supervisor"}}}, "resource": {"id": 106, "owner": {"id": 272}, "organization": {"id": 160}, "project": {"owner": {"id": 424}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 23, "privilege": "worker"}, "organization": {"id": 102, "owner": {"id": 235}, "user": {"role": "supervisor"}}}, "resource": {"id": 102, "owner": {"id": 215}, "organization": {"id": 384}, "project": {"owner": {"id": 468}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 56, "privilege": "worker"}, "organization": {"id": 110, "owner": {"id": 268}, "user": {"role": "worker"}}}, "resource": {"id": 126, "owner": {"id": 231}, "organization": {"id": 110}, "project": {"owner": {"id": 474}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 5, "privilege": "worker"}, "organization": {"id": 162, "owner": {"id": 202}, "user": {"role": "worker"}}}, "resource": {"id": 177, "owner": {"id": 241}, "organization": {"id": 351}, "project": {"owner": {"id": 475}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 3, "privilege": "worker"}, "organization": {"id": 167, "owner": {"id": 240}, "user": {"role": null}}}, "resource": {"id": 143, "owner": {"id": 255}, "organization": {"id": 167}, "project": {"owner": {"id": 453}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 12, "privilege": "worker"}, "organization": {"id": 169, "owner": {"id": 221}, "user": {"role": null}}}, "resource": {"id": 148, "owner": {"id": 232}, "organization": {"id": 311}, "project": {"owner": {"id": 414}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_OWNER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 98, "privilege": "none"}, "organization": {"id": 111, "owner": {"id": 98}, "user": {"role": "owner"}}}, "resource": {"id": 136, "owner": {"id": 218}, "organization": {"id": 111}, "project": {"owner": {"id": 432}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_OWNER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 7, "privilege": "none"}, "organization": {"id": 178, "owner": {"id": 7}, "user": {"role": "owner"}}}, "resource": {"id": 139, "owner": {"id": 252}, "organization": {"id": 386}, "project": {"owner": {"id": 421}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_MAINTAINER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 38, "privilege": "none"}, "organization": {"id": 149, "owner": {"id": 212}, "user": {"role": "maintainer"}}}, "resource": {"id": 123, "owner": {"id": 203}, "organization": {"id": 149}, "project": {"owner": {"id": 407}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_MAINTAINER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 33, "privilege": "none"}, "organization": {"id": 148, "owner": {"id": 215}, "user": {"role": "maintainer"}}}, "resource": {"id": 140, "owner": {"id": 218}, "organization": {"id": 352}, "project": {"owner": {"id": 441}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 76, "privilege": "none"}, "organization": {"id": 175, "owner": {"id": 230}, "user": {"role": "supervisor"}}}, "resource": {"id": 106, "owner": {"id": 225}, "organization": {"id": 175}, "project": {"owner": {"id": 496}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_SUPERVISOR_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 49, "privilege": "none"}, "organization": {"id": 114, "owner": {"id": 234}, "user": {"role": "supervisor"}}}, "resource": {"id": 163, "owner": {"id": 265}, "organization": {"id": 396}, "project": {"owner": {"id": 464}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_WORKER_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 52, "privilege": "none"}, "organization": {"id": 108, "owner": {"id": 261}, "user": {"role": "worker"}}}, "resource": {"id": 107, "owner": {"id": 240}, "organization": {"id": 108}, "project": {"owner": {"id": 470}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_WORKER_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 2, "privilege": "none"}, "organization": {"id": 134, "owner": {"id": 270}, "user": {"role": "worker"}}}, "resource": {"id": 103, "owner": {"id": 252}, "organization": {"id": 381}, "project": {"owner": {"id": 429}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_NONE_resource_project_same_org_TRUE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 97, "privilege": "none"}, "organization": {"id": 171, "owner": {"id": 283}, "user": {"role": null}}}, "resource": {"id": 175, "owner": {"id": 286}, "organization": {"id": 171}, "project": {"owner": {"id": 431}}}} +} + +test_scope_UPDATE_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_NONE_resource_project_same_org_FALSE { + not allow with input as {"scope": "update", "auth": {"user": {"id": 57, "privilege": "none"}, "organization": {"id": 110, "owner": {"id": 281}, "user": {"role": null}}}, "resource": {"id": 116, "owner": {"id": 238}, "organization": {"id": 372}, "project": {"owner": {"id": 452}}}} +} + + +# import csv +# import json +# import random +# from itertools import product +# +# NAME = "webhooks" +# +# +# def read_rules(name): +# rules = [] +# with open(f"{name}.csv") as f: +# reader = csv.DictReader(f) +# for row in reader: +# row = {k.lower(): v.lower().replace("n/a", "na") for k, v in row.items()} +# row["limit"] = row["limit"].replace("none", "None") +# found = False +# for col, val in row.items(): +# if col in ["limit", "method", "url", "resource"]: +# continue +# complex_val = [v.strip() for v in val.split(",")] +# if len(complex_val) > 1: +# found = True +# for item in complex_val: +# new_row = row.copy() +# new_row[col] = item +# rules.append(new_row) +# if not found: +# rules.append(row) +# return rules +# +# +# random.seed(42) +# simple_rules = read_rules(NAME) +# SCOPES = list({rule["scope"] for rule in simple_rules}) +# CONTEXTS = ["sandbox", "organization"] +# OWNERSHIPS = ["project:owner", "owner", "none"] +# GROUPS = ["admin", "business", "user", "worker", "none"] +# ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None] +# SAME_ORG = [True, False] +# +# +# def RESOURCES(scope): +# if scope == "list": +# return [None] +# elif scope == "create@project": +# return [ +# { +# "owner": {"id": random.randrange(100, 200)}, +# "assignee": {"id": random.randrange(200, 300)}, +# "organization": {"id": random.randrange(300, 400)}, +# "project": {"owner": {"id": random.randrange(400, 500)}}, +# "num_resources": count, +# } +# for count in (0, 3, 10) +# ] +# elif scope == "create@organization": +# return [ +# { +# "owner": {"id": random.randrange(100, 200)}, +# "assignee": {"id": random.randrange(200, 300)}, +# "organization": {"id": random.randrange(300, 400)}, +# "project": None, +# "num_resources": count, +# } +# for count in (0, 3, 10) +# ] +# else: +# return [ +# { +# "id": random.randrange(100, 200), +# "owner": {"id": random.randrange(200, 300)}, +# "organization": {"id": random.randrange(300, 400)}, +# "project": {"owner": {"id": random.randrange(400, 500)}}, +# } +# ] +# +# +# def is_same_org(org1, org2): +# if org1 is not None and org2 is not None: +# return org1["id"] == org2["id"] +# elif org1 is None and org2 is None: +# return True +# return False +# +# +# def eval_rule(scope, context, ownership, privilege, membership, data): +# if privilege == "admin": +# return True +# +# rules = list( +# filter( +# lambda r: scope == r["scope"] +# and (r["context"] == "na" or context == r["context"]) +# and (r["ownership"] == "na" or ownership == r["ownership"]) +# and ( +# r["membership"] == "na" +# or ORG_ROLES.index(membership) <= ORG_ROLES.index(r["membership"]) +# ) +# and GROUPS.index(privilege) <= GROUPS.index(r["privilege"]), +# simple_rules, +# ) +# ) +# +# resource = data["resource"] +# +# rules = list( +# filter( +# lambda r: not r["limit"] or eval(r["limit"], {"resource": resource}), rules +# ) +# ) +# if ( +# not is_same_org(data["auth"]["organization"], data["resource"]["organization"]) +# and context != "sandbox" +# ): +# return False +# return bool(rules) +# +# +# def get_data(scope, context, ownership, privilege, membership, resource, same_org): +# data = { +# "scope": scope, +# "auth": { +# "user": {"id": random.randrange(0, 100), "privilege": privilege}, +# "organization": { +# "id": random.randrange(100, 200), +# "owner": {"id": random.randrange(200, 300)}, +# "user": {"role": membership}, +# } +# if context == "organization" +# else None, +# }, +# "resource": resource, +# } +# +# user_id = data["auth"]["user"]["id"] +# +# if context == "organization": +# org_id = data["auth"]["organization"]["id"] +# if data["auth"]["organization"]["user"]["role"] == "owner": +# data["auth"]["organization"]["owner"]["id"] = user_id +# if same_org: +# data["resource"]["organization"]["id"] = org_id +# +# if ownership == "owner": +# data["resource"]["owner"]["id"] = user_id +# +# if ownership == "project:owner": +# data["resource"]["project"]["owner"]["id"] = user_id +# +# return data +# +# +# def _get_name(prefix, **kwargs): +# name = prefix +# for k, v in kwargs.items(): +# prefix = "_" + str(k) +# if isinstance(v, dict): +# if "id" in v: +# v = v.copy() +# v.pop("id") +# if v: +# name += _get_name(prefix, **v) +# else: +# name += "".join( +# map( +# lambda c: c if c.isalnum() else {"@": "_IN_"}.get(c, "_"), +# f"{prefix}_{str(v).upper()}", +# ) +# ) +# return name +# +# +# def get_name(scope, context, ownership, privilege, membership, resource, same_org): +# return _get_name("test", **locals()) +# +# +# def is_valid(scope, context, ownership, privilege, membership, resource, same_org): +# if context == "sandbox" and membership: +# return False +# if scope == "list" and ownership != "None": +# return False +# if context == "sandbox" and not same_org: +# return False +# if scope.startswith("create") and ownership != "None": +# return False +# +# return True +# +# +# def gen_test_rego(name): +# with open(f"{name}_test.gen.rego", "wt") as f: +# f.write(f"package {name}\n\n") +# for scope, context, ownership, privilege, membership, same_org in product( +# SCOPES, CONTEXTS, OWNERSHIPS, GROUPS, ORG_ROLES, SAME_ORG +# ): +# for resource in RESOURCES(scope): +# +# if not is_valid( +# scope, context, ownership, privilege, membership, resource, same_org +# ): +# continue +# +# data = get_data( +# scope, context, ownership, privilege, membership, resource, same_org +# ) +# test_name = get_name( +# scope, context, ownership, privilege, membership, resource, same_org +# ) +# result = eval_rule( +# scope, context, ownership, privilege, membership, data +# ) +# +# f.write( +# "{test_name} {{\n {allow} with input as {data}\n}}\n\n".format( +# test_name=test_name, +# allow="allow" if result else "not allow", +# data=json.dumps(data), +# ) +# ) +# +# +# gen_test_rego(NAME) +# \ No newline at end of file diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index dc3421fa6683..131be94d49e8 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -1,3 +1,8 @@ +# Copyright (C) 2021-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + from distutils.util import strtobool from django.conf import settings from django.db import models @@ -51,6 +56,9 @@ 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): + return self.membership.organization_id + def send(self): if not strtobool(settings.ORG_INVITATION_CONFIRM): self.accept(self.created_date) diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index f84bc89e472c..4c94c21a905a 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -1,4 +1,5 @@ # Copyright (C) 2021-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -7,6 +8,8 @@ from django.utils.crypto import get_random_string from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view +from cvat.apps.engine.mixins import PartialUpdateModelMixin, DestroyModelMixin +from cvat.apps.webhooks.signals import signal_create from cvat.apps.iam.permissions import ( InvitationPermission, MembershipPermission, OrganizationPermission) @@ -25,7 +28,7 @@ '200': OrganizationReadSerializer, }), list=extend_schema( - summary='Method returns a paginated list of organizatins according to query parameters', + summary='Method returns a paginated list of organizations according to query parameters', responses={ '200': OrganizationReadSerializer(many=True), }), @@ -50,7 +53,13 @@ '204': OpenApiResponse(description='The organization has been deleted'), }) ) -class OrganizationViewSet(viewsets.ModelViewSet): +class OrganizationViewSet(viewsets.GenericViewSet, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + PartialUpdateModelMixin, + ): queryset = Organization.objects.all() search_fields = ('name', 'owner') filter_fields = list(search_fields) + ['id', 'slug'] @@ -110,8 +119,8 @@ class Meta: '204': OpenApiResponse(description='The membership has been deleted'), }) ) -class MembershipViewSet(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, - mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): +class MembershipViewSet(mixins.RetrieveModelMixin, DestroyModelMixin, + mixins.ListModelMixin, PartialUpdateModelMixin, viewsets.GenericViewSet): queryset = Membership.objects.all() ordering = '-id' http_method_names = ['get', 'patch', 'delete', 'head', 'options'] @@ -165,7 +174,13 @@ def get_queryset(self): '204': OpenApiResponse(description='The invitation has been deleted'), }) ) -class InvitationViewSet(viewsets.ModelViewSet): +class InvitationViewSet(viewsets.GenericViewSet, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + DestroyModelMixin, + ): queryset = Invitation.objects.all() http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options'] iam_organization_field = 'membership__organization' @@ -194,6 +209,7 @@ def perform_create(self, serializer): 'organization': self.request.iam_context['organization'] } serializer.save(**extra_kwargs) + signal_create.send(self, instance=serializer.instance) def perform_update(self, serializer): if 'accepted' in self.request.query_params: diff --git a/cvat/apps/webhooks/__init__.py b/cvat/apps/webhooks/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat/apps/webhooks/apps.py b/cvat/apps/webhooks/apps.py new file mode 100644 index 000000000000..ac15da359b10 --- /dev/null +++ b/cvat/apps/webhooks/apps.py @@ -0,0 +1,12 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from django.apps import AppConfig + + +class WebhooksConfig(AppConfig): + name = "cvat.apps.webhooks" + + def ready(self): + from . import signals # pylint: disable=unused-import diff --git a/cvat/apps/webhooks/event_type.py b/cvat/apps/webhooks/event_type.py new file mode 100644 index 000000000000..4c74810f54dc --- /dev/null +++ b/cvat/apps/webhooks/event_type.py @@ -0,0 +1,58 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from .models import WebhookTypeChoice + + +def event_name(action, resource): + return f"{action}:{resource}" + + +class Events: + RESOURCES = { + "project": ["create", "update", "delete"], + "task": ["create", "update", "delete"], + "issue": ["create", "update", "delete"], + "comment": ["create", "update", "delete"], + "invitation": ["create", "delete"], # TO-DO: implement invitation_updated, + "membership": ["update", "delete"], + "job": ["update"], + "organization": ["update"], + } + + @classmethod + def select(cls, resources): + return [ + f"{event_name(action, resource)}" + for resource in resources + for action in cls.RESOURCES.get(resource, []) + ] + + +class EventTypeChoice: + @classmethod + def choices(cls): + return sorted((val, val.upper()) for val in AllEvents.events) + + +class AllEvents: + webhook_type = "all" + events = list( + event_name(action, resource) + for resource, actions in Events.RESOURCES.items() + for action in actions + ) + + +class ProjectEvents: + webhook_type = WebhookTypeChoice.PROJECT + events = [event_name("update", "project")] \ + + Events.select(["job", "task", "issue", "comment"]) + + +class OrganizationEvents: + webhook_type = WebhookTypeChoice.ORGANIZATION + events = [event_name("update", "organization")] \ + + Events.select(["membership", "invitation", "project"]) \ + + ProjectEvents.events diff --git a/cvat/apps/webhooks/migrations/0001_initial.py b/cvat/apps/webhooks/migrations/0001_initial.py new file mode 100644 index 000000000000..fe8f296b0514 --- /dev/null +++ b/cvat/apps/webhooks/migrations/0001_initial.py @@ -0,0 +1,64 @@ +# Generated by Django 3.2.15 on 2022-09-19 08:26 + +import cvat.apps.webhooks.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('engine', '0060_alter_label_parent'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('organizations', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Webhook', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('target_url', models.URLField()), + ('description', models.CharField(blank=True, default='', max_length=128)), + ('events', models.CharField(default='', max_length=4096)), + ('type', models.CharField(choices=[('organization', 'ORGANIZATION'), ('project', 'PROJECT')], max_length=16)), + ('content_type', models.CharField(choices=[('application/json', 'JSON')], default=cvat.apps.webhooks.models.WebhookContentTypeChoice['JSON'], max_length=64)), + ('secret', models.CharField(blank=True, default='', max_length=64)), + ('is_active', models.BooleanField(default=True)), + ('enable_ssl', models.BooleanField(default=True)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='organizations.organization')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('project', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='engine.project')), + ], + options={ + 'default_permissions': (), + }, + ), + migrations.CreateModel( + name='WebhookDelivery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event', models.CharField(max_length=64)), + ('status_code', models.CharField(max_length=128, null=True)), + ('redelivery', models.BooleanField(default=False)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('changed_fields', models.CharField(default='', max_length=4096)), + ('request', models.JSONField(default=dict)), + ('response', models.JSONField(default=dict)), + ('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='deliveries', to='webhooks.webhook')), + ], + options={ + 'default_permissions': (), + }, + ), + migrations.AddConstraint( + model_name='webhook', + constraint=models.CheckConstraint(check=models.Q(models.Q(('project_id__isnull', False), ('type', 'project')), models.Q(('organization_id__isnull', False), ('project_id__isnull', True), ('type', 'organization')), _connector='OR'), name='webhooks_project_or_organization'), + ), + ] diff --git a/cvat/apps/webhooks/migrations/0002_alter_webhookdelivery_status_code.py b/cvat/apps/webhooks/migrations/0002_alter_webhookdelivery_status_code.py new file mode 100644 index 000000000000..fd1a2397d249 --- /dev/null +++ b/cvat/apps/webhooks/migrations/0002_alter_webhookdelivery_status_code.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.15 on 2022-09-27 12:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webhooks', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='webhookdelivery', + name='status_code', + field=models.IntegerField(choices=[('CONTINUE', 100), ('SWITCHING_PROTOCOLS', 101), ('PROCESSING', 102), ('OK', 200), ('CREATED', 201), ('ACCEPTED', 202), ('NON_AUTHORITATIVE_INFORMATION', 203), ('NO_CONTENT', 204), ('RESET_CONTENT', 205), ('PARTIAL_CONTENT', 206), ('MULTI_STATUS', 207), ('ALREADY_REPORTED', 208), ('IM_USED', 226), ('MULTIPLE_CHOICES', 300), ('MOVED_PERMANENTLY', 301), ('FOUND', 302), ('SEE_OTHER', 303), ('NOT_MODIFIED', 304), ('USE_PROXY', 305), ('TEMPORARY_REDIRECT', 307), ('PERMANENT_REDIRECT', 308), ('BAD_REQUEST', 400), ('UNAUTHORIZED', 401), ('PAYMENT_REQUIRED', 402), ('FORBIDDEN', 403), ('NOT_FOUND', 404), ('METHOD_NOT_ALLOWED', 405), ('NOT_ACCEPTABLE', 406), ('PROXY_AUTHENTICATION_REQUIRED', 407), ('REQUEST_TIMEOUT', 408), ('CONFLICT', 409), ('GONE', 410), ('LENGTH_REQUIRED', 411), ('PRECONDITION_FAILED', 412), ('REQUEST_ENTITY_TOO_LARGE', 413), ('REQUEST_URI_TOO_LONG', 414), ('UNSUPPORTED_MEDIA_TYPE', 415), ('REQUESTED_RANGE_NOT_SATISFIABLE', 416), ('EXPECTATION_FAILED', 417), ('MISDIRECTED_REQUEST', 421), ('UNPROCESSABLE_ENTITY', 422), ('LOCKED', 423), ('FAILED_DEPENDENCY', 424), ('UPGRADE_REQUIRED', 426), ('PRECONDITION_REQUIRED', 428), ('TOO_MANY_REQUESTS', 429), ('REQUEST_HEADER_FIELDS_TOO_LARGE', 431), ('UNAVAILABLE_FOR_LEGAL_REASONS', 451), ('INTERNAL_SERVER_ERROR', 500), ('NOT_IMPLEMENTED', 501), ('BAD_GATEWAY', 502), ('SERVICE_UNAVAILABLE', 503), ('GATEWAY_TIMEOUT', 504), ('HTTP_VERSION_NOT_SUPPORTED', 505), ('VARIANT_ALSO_NEGOTIATES', 506), ('INSUFFICIENT_STORAGE', 507), ('LOOP_DETECTED', 508), ('NOT_EXTENDED', 510), ('NETWORK_AUTHENTICATION_REQUIRED', 511)], default=None, null=True), + ), + ] diff --git a/cvat/apps/webhooks/migrations/__init__.py b/cvat/apps/webhooks/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat/apps/webhooks/models.py b/cvat/apps/webhooks/models.py new file mode 100644 index 000000000000..c563657567e7 --- /dev/null +++ b/cvat/apps/webhooks/models.py @@ -0,0 +1,107 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from enum import Enum +from http import HTTPStatus + +from django.contrib.auth.models import User +from django.db import models + +from cvat.apps.engine.models import Project +from cvat.apps.organizations.models import Organization + + +class WebhookTypeChoice(str, Enum): + ORGANIZATION = "organization" + PROJECT = "project" + + @classmethod + def choices(cls): + return tuple((x.value, x.name) for x in cls) + + def __str__(self): + return self.value + + +class WebhookContentTypeChoice(str, Enum): + JSON = "application/json" + + @classmethod + def choices(cls): + return tuple((x.value, x.name) for x in cls) + + def __str__(self): + return self.value + + +class Webhook(models.Model): + target_url = models.URLField() + description = models.CharField(max_length=128, default="", blank=True) + + events = models.CharField(max_length=4096, default="") + type = models.CharField(max_length=16, choices=WebhookTypeChoice.choices()) + content_type = models.CharField( + max_length=64, + choices=WebhookContentTypeChoice.choices(), + default=WebhookContentTypeChoice.JSON, + ) + secret = models.CharField(max_length=64, blank=True, default="") + + is_active = models.BooleanField(default=True) + enable_ssl = models.BooleanField(default=True) + + created_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) + + # questionable: should we keep webhook if owner has been deleted? + owner = models.ForeignKey( + User, null=True, blank=True, on_delete=models.SET_NULL, related_name="+" + ) + project = models.ForeignKey( + Project, null=True, on_delete=models.CASCADE, related_name="+" + ) + organization = models.ForeignKey( + Organization, null=True, on_delete=models.CASCADE, related_name="+" + ) + + class Meta: + default_permissions = () + constraints = [ + models.CheckConstraint( + name="webhooks_project_or_organization", + check=( + models.Q( + type=WebhookTypeChoice.PROJECT.value, project_id__isnull=False + ) + | models.Q( + type=WebhookTypeChoice.ORGANIZATION.value, + project_id__isnull=True, + organization_id__isnull=False, + ) + ), + ) + ] + + +class WebhookDelivery(models.Model): + webhook = models.ForeignKey( + Webhook, on_delete=models.CASCADE, related_name="deliveries" + ) + event = models.CharField(max_length=64) + + status_code = models.IntegerField( + choices=tuple((x.name, x.value) for x in HTTPStatus), null=True, default=None + ) + redelivery = models.BooleanField(default=False) + + created_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) + + changed_fields = models.CharField(max_length=4096, default="") + + request = models.JSONField(default=dict) + response = models.JSONField(default=dict) + + class Meta: + default_permissions = () diff --git a/cvat/apps/webhooks/serializers.py b/cvat/apps/webhooks/serializers.py new file mode 100644 index 000000000000..653659e68ef3 --- /dev/null +++ b/cvat/apps/webhooks/serializers.py @@ -0,0 +1,152 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from .event_type import EventTypeChoice, ProjectEvents, OrganizationEvents +from .models import ( + Webhook, + WebhookContentTypeChoice, + WebhookTypeChoice, + WebhookDelivery, +) +from rest_framework import serializers +from cvat.apps.engine.serializers import BasicUserSerializer, WriteOnceMixin + + +class EventTypeValidator: + requires_context = True + + def get_webhook_type(self, attrs, serializer): + if serializer.instance is not None: + return serializer.instance.type + return attrs.get("type") + + def __call__(self, attrs, serializer): + if attrs.get("events") is not None: + webhook_type = self.get_webhook_type(attrs, serializer) + events = set(EventTypesSerializer().to_representation(attrs["events"])) + if ( + webhook_type == WebhookTypeChoice.PROJECT + and not events.issubset(set(ProjectEvents.events)) + ) or ( + webhook_type == WebhookTypeChoice.ORGANIZATION + and not events.issubset(set(OrganizationEvents.events)) + ): + raise serializers.ValidationError( + f"Invalid events list for {webhook_type} webhook" + ) + + +class EventTypesSerializer(serializers.MultipleChoiceField): + def __init__(self, *args, **kwargs): + super().__init__(choices=EventTypeChoice.choices(), *args, **kwargs) + + def to_representation(self, value): + if isinstance(value, list): + return sorted(super().to_representation(value)) + return sorted(list(super().to_representation(value.split(",")))) + + def to_internal_value(self, data): + return ",".join(super().to_internal_value(data)) + + +class EventsSerializer(serializers.Serializer): + webhook_type = serializers.ChoiceField(choices=WebhookTypeChoice.choices()) + events = EventTypesSerializer() + + +class WebhookReadSerializer(serializers.ModelSerializer): + owner = BasicUserSerializer(read_only=True, required=False) + + events = EventTypesSerializer(read_only=True) + + type = serializers.ChoiceField(choices=WebhookTypeChoice.choices()) + content_type = serializers.ChoiceField(choices=WebhookContentTypeChoice.choices()) + + last_status = serializers.IntegerField( + source="deliveries.last.status_code", read_only=True + ) + + last_delivery_date = serializers.DateTimeField( + source="deliveries.last.updated_date", read_only=True + ) + + class Meta: + model = Webhook + fields = ( + "id", + "url", + "target_url", + "description", + "type", + "content_type", + "is_active", + "enable_ssl", + "created_date", + "updated_date", + "owner", + "project", + "organization", + "events", + "last_status", + "last_delivery_date", + ) + read_only_fields = fields + + +class WebhookWriteSerializer(WriteOnceMixin, serializers.ModelSerializer): + events = EventTypesSerializer(write_only=True) + + # Q: should be owner_id required or not? + owner_id = serializers.IntegerField( + write_only=True, allow_null=True, required=False + ) + + project_id = serializers.IntegerField( + write_only=True, allow_null=True, required=False + ) + + def to_representation(self, instance): + serializer = WebhookReadSerializer(instance, context=self.context) + return serializer.data + + class Meta: + model = Webhook + fields = ( + "target_url", + "description", + "type", + "content_type", + "secret", + "is_active", + "enable_ssl", + "owner_id", + "project_id", + "events", + ) + write_once_fields = ("type", "owner_id", "project_id") + validators = [EventTypeValidator()] + + def create(self, validated_data): + db_webhook = Webhook.objects.create(**validated_data) + return db_webhook + + +class WebhookDeliveryReadSerializer(serializers.ModelSerializer): + webhook_id = serializers.IntegerField(read_only=True) + + class Meta: + model = WebhookDelivery + fields = ( + "id", + "webhook_id", + "event", + "status_code", + "redelivery", + "created_date", + "updated_date", + "changed_fields", + "request", + "response", + ) + read_only_fields = fields diff --git a/cvat/apps/webhooks/signals.py b/cvat/apps/webhooks/signals.py new file mode 100644 index 000000000000..ee152bb82cfc --- /dev/null +++ b/cvat/apps/webhooks/signals.py @@ -0,0 +1,221 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import hashlib +import hmac +from http import HTTPStatus +import json + +import django_rq +import requests +from django.dispatch import Signal, receiver + +from cvat.apps.engine.models import Project +from cvat.apps.engine.serializers import BasicUserSerializer +from cvat.apps.organizations.models import Organization + +from .event_type import EventTypeChoice, event_name +from .models import Webhook, WebhookDelivery, WebhookTypeChoice + +WEBHOOK_TIMEOUT = 10 +RESPONSE_SIZE_LIMIT = 1 * 1024 * 1024 # 1 MB + +signal_update = Signal() +signal_create = Signal() +signal_delete = Signal() +signal_redelivery = Signal() +signal_ping = Signal() + + +def send_webhook(webhook, payload, delivery): + headers = {} + if webhook.secret: + headers["X-Signature-256"] = ( + "sha256=" + + hmac.new( + webhook.secret.encode("utf-8"), + (json.dumps(payload) + "\n").encode("utf-8"), + digestmod=hashlib.sha256, + ).hexdigest() + ) + + response_body = None + try: + response = requests.post( + webhook.target_url, + json=payload, + verify=webhook.enable_ssl, + headers=headers, + timeout=WEBHOOK_TIMEOUT, + stream=True, + ) + status_code = response.status_code + response_body = response.raw.read(RESPONSE_SIZE_LIMIT + 1, decode_content=True) + except requests.ConnectionError: + status_code = HTTPStatus.BAD_GATEWAY + except requests.Timeout: + status_code = HTTPStatus.GATEWAY_TIMEOUT + + setattr(delivery, "status_code", status_code) + if response_body is not None and len(response_body) < RESPONSE_SIZE_LIMIT + 1: + setattr(delivery, "response", response_body.decode("utf-8")) + + delivery.save() + + +def add_to_queue(webhook, payload, redelivery=False): + delivery = WebhookDelivery.objects.create( + webhook_id=webhook.id, + event=payload["event"], + status_code=None, + changed_fields=",".join(list(payload.get("before_update", {}).keys())), + redelivery=redelivery, + request=payload, + response="", + ) + + queue = django_rq.get_queue("webhooks") + queue.enqueue_call(func=send_webhook, args=(webhook, payload, delivery)) + + return delivery + + +def select_webhooks(project_id, org_id, event): + selected_webhooks = [] + if org_id is not None: + webhooks = Webhook.objects.filter( + is_active=True, + events__contains=event, + type=WebhookTypeChoice.ORGANIZATION, + organization=org_id, + ) + selected_webhooks += list(webhooks) + + if project_id is not None: + webhooks = Webhook.objects.filter( + is_active=True, + events__contains=event, + type=WebhookTypeChoice.PROJECT, + organization=org_id, + project=project_id, + ) + selected_webhooks += list(webhooks) + + return selected_webhooks + + +def payload(data, request): + return { + **data, + "sender": BasicUserSerializer(request.user, context={"request": request}).data, + } + + +def project_id(instance): + if isinstance(instance, Project): + return instance.id + + try: + pid = getattr(instance, "project_id", None) + if pid is None: + return instance.get_project_id() + return pid + except Exception: + return None + + +def organization_id(instance): + if isinstance(instance, Organization): + return instance.id + + try: + oid = getattr(instance, "organization_id", None) + if oid is None: + return instance.get_organization_id() + return oid + except Exception: + return None + + +@receiver(signal_update) +def update(sender, instance=None, old_values=None, **kwargs): + event = event_name("update", sender.basename) + if event not in map(lambda a: a[0], EventTypeChoice.choices()): + return + + serializer = sender.get_serializer_class()( + instance=instance, context={"request": sender.request} + ) + + pid = project_id(instance) + oid = organization_id(instance) + + if not any((oid, pid)): + return + + data = { + "event": event, + sender.basename: serializer.data, + "before_update": old_values, + } + + for webhook in select_webhooks(pid, oid, event): + data.update({"webhook_id": webhook.id}) + add_to_queue(webhook, payload(data, sender.request)) + + +@receiver(signal_create) +def resource_created(sender, instance=None, **kwargs): + event = event_name("create", sender.basename) + if event not in map(lambda a: a[0], EventTypeChoice.choices()): + return + + pid = project_id(instance) + oid = organization_id(instance) + if not any((oid, pid)): + return + + serializer = sender.get_serializer_class()( + instance=instance, context={"request": sender.request} + ) + + data = {"event": event, sender.basename: serializer.data} + + for webhook in select_webhooks(pid, oid, event): + data.update({"webhook_id": webhook.id}) + add_to_queue(webhook, payload(data, sender.request)) + + +@receiver(signal_delete) +def resource_deleted(sender, instance=None, **kwargs): + event = event_name("delete", sender.basename) + if event not in map(lambda a: a[0], EventTypeChoice.choices()): + return + + pid = project_id(instance) + oid = organization_id(instance) + if not any((oid, pid)): + return + + serializer = sender.get_serializer_class()( + instance=instance, context={"request": sender.request} + ) + + data = {"event": event, sender.basename: serializer.data} + + for webhook in select_webhooks(pid, oid, event): + data.update({"webhook_id": webhook.id}) + add_to_queue(webhook, payload(data, sender.request)) + + +@receiver(signal_redelivery) +def redelivery(sender, data=None, **kwargs): + add_to_queue(sender.get_object(), data, redelivery=True) + + +@receiver(signal_ping) +def ping(sender, serializer, **kwargs): + data = {"event": "ping", "webhook": serializer.data} + delivery = add_to_queue(serializer.instance, payload(data, sender.request)) + return delivery diff --git a/cvat/apps/webhooks/urls.py b/cvat/apps/webhooks/urls.py new file mode 100644 index 000000000000..c309df746f96 --- /dev/null +++ b/cvat/apps/webhooks/urls.py @@ -0,0 +1,11 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from rest_framework.routers import DefaultRouter +from .views import WebhookViewSet + +router = DefaultRouter(trailing_slash=False) +router.register("webhooks", WebhookViewSet) + +urlpatterns = router.urls diff --git a/cvat/apps/webhooks/views.py b/cvat/apps/webhooks/views.py new file mode 100644 index 000000000000..b39e1bc018a9 --- /dev/null +++ b/cvat/apps/webhooks/views.py @@ -0,0 +1,195 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +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.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 .signals import signal_ping, signal_redelivery + + +@extend_schema(tags=["webhooks"]) +@extend_schema_view( + retrieve=extend_schema( + summary="Method returns details of a webhook", + responses={"200": WebhookReadSerializer}, + ), + list=extend_schema( + summary="Method returns a paginated list of webhook according to query parameters", + responses={"200": WebhookReadSerializer(many=True)}, + ), + update=extend_schema( + summary="Method updates a webhook by id", + responses={"200": WebhookWriteSerializer}, + ), + partial_update=extend_schema( + summary="Methods does a partial update of chosen fields in a webhook", + responses={"200": WebhookWriteSerializer}, + ), + create=extend_schema( + summary="Method creates a webhook", responses={"201": WebhookWriteSerializer} + ), + destroy=extend_schema( + summary="Method deletes a webhook", + responses={"204": OpenApiResponse(description="The webhook has been deleted")}, + ), +) +class WebhookViewSet(viewsets.ModelViewSet): + queryset = Webhook.objects.all() + ordering = "-id" + http_method_names = ["get", "post", "delete", "patch", "put"] + + search_fields = ("target_url", "owner", "type", "description") + filter_fields = list(search_fields) + ["id", "project_id", "updated_date"] + ordering_fields = filter_fields + lookup_fields = {"owner": "owner__username"} + iam_organization_field = "organization" + + def get_serializer_class(self): + if self.request.path.endswith("redelivery") or self.request.path.endswith( + "ping" + ): + return None + else: + if self.request.method in SAFE_METHODS: + return WebhookReadSerializer + else: + return WebhookWriteSerializer + + def get_queryset(self): + queryset = super().get_queryset() + if self.action == "list": + perm = WebhookPermission.create_scope_list(self.request) + queryset = perm.filter(queryset) + + return queryset + + def perform_create(self, serializer): + serializer.save( + owner=self.request.user, + organization=self.request.iam_context["organization"], + ) + + @extend_schema( + summary="Method return a list of available webhook events", + parameters=[ + OpenApiParameter( + "type", + description="Type of webhook", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.STR, + required=False, + ) + ], + responses={"200": OpenApiResponse(EventsSerializer)}, + ) + @action(detail=False, methods=["GET"], serializer_class=EventsSerializer) + def events(self, request): + webhook_type = request.query_params.get("type", "all") + events = None + if webhook_type == "all": + events = AllEvents + elif webhook_type == WebhookTypeChoice.PROJECT: + events = ProjectEvents + elif webhook_type == WebhookTypeChoice.ORGANIZATION: + events = OrganizationEvents + + if events is None: + return Response( + "Incorrect value of type parameter", status=status.HTTP_400_BAD_REQUEST + ) + + return Response(EventsSerializer().to_representation(events)) + + @extend_schema( + summary="Method return a list of deliveries for a specific webhook", + responses={"200": WebhookDeliveryReadSerializer(many=True)}, + ) + @action( + detail=True, methods=["GET"], serializer_class=WebhookDeliveryReadSerializer + ) + def deliveries(self, request, pk): + self.get_object() + queryset = WebhookDelivery.objects.filter(webhook_id=pk).order_by( + "-updated_date" + ) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = WebhookDeliveryReadSerializer( + page, many=True, context={"request": request} + ) + return self.get_paginated_response(serializer.data) + + serializer = WebhookDeliveryReadSerializer( + queryset, many=True, context={"request": request} + ) + + return Response(serializer.data) + + @extend_schema( + summary="Method return a specific delivery for a specific webhook", + responses={"200": WebhookDeliveryReadSerializer}, + ) + @action( + detail=True, + methods=["GET"], + url_path=r"deliveries/(?P\d+)", + serializer_class=WebhookDeliveryReadSerializer, + ) + def retrieve_delivery(self, request, pk, delivery_id): + self.get_object() + queryset = WebhookDelivery.objects.get(webhook_id=pk, id=delivery_id) + serializer = WebhookDeliveryReadSerializer( + queryset, context={"request": request} + ) + return Response(serializer.data) + + @extend_schema(summary="Method redeliver a specific webhook delivery") + @action( + detail=True, + methods=["POST"], + url_path=r"deliveries/(?P\d+)/redelivery", + ) + def redelivery(self, request, pk, delivery_id): + delivery = WebhookDelivery.objects.get(webhook_id=pk, id=delivery_id) + signal_redelivery.send(sender=self, data=delivery.request) + + # Questionable: should we provide a body for this response? + return Response({}) + + @extend_schema( + summary="Method send ping webhook", + responses={"200": WebhookDeliveryReadSerializer}, + ) + @action( + detail=True, methods=["POST"], serializer_class=WebhookDeliveryReadSerializer + ) + def ping(self, request, pk): + instance = self.get_object() + serializer = WebhookReadSerializer(instance, context={"request": request}) + + delivery = signal_ping.send(sender=self, serializer=serializer)[0][1] + serializer = WebhookDeliveryReadSerializer( + delivery, context={"request": request} + ) + return Response(serializer.data) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 9cac8971d2fb..ea252b11ef3e 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -128,6 +128,7 @@ def add_ssh_keys(): 'cvat.apps.restrictions', 'cvat.apps.lambda_manager', 'cvat.apps.opencv', + 'cvat.apps.webhooks', ] SITE_ID = 1 @@ -277,6 +278,12 @@ def add_ssh_keys(): 'PORT': 6379, 'DB': 0, 'DEFAULT_TIMEOUT': '24h' + }, + 'webhooks': { + 'HOST': 'localhost', + 'PORT': 6379, + 'DB': 0, + 'DEFAULT_TIMEOUT': '1h' } } diff --git a/cvat/urls.py b/cvat/urls.py index 5eb86c8fe192..09eed3e9cfea 100644 --- a/cvat/urls.py +++ b/cvat/urls.py @@ -40,5 +40,8 @@ if apps.is_installed('cvat.apps.opencv'): urlpatterns.append(path('opencv/', include('cvat.apps.opencv.urls'))) +if apps.is_installed('cvat.apps.webhooks'): + urlpatterns.append(path('api/', include('cvat.apps.webhooks.urls'))) + if apps.is_installed('silk'): urlpatterns.append(path('profiler/', include('silk.urls'))) diff --git a/docker-compose.yml b/docker-compose.yml index de616828f49d..80605dd43023 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,6 +117,27 @@ services: networks: - cvat + cvat_worker_webhooks: + container_name: cvat_worker_webhooks + image: cvat/server:${CVAT_VERSION:-dev} + restart: always + depends_on: + - cvat_redis + - cvat_db + - cvat_opa + environment: + CVAT_REDIS_HOST: 'cvat_redis' + CVAT_POSTGRES_HOST: 'cvat_db' + no_proxy: elasticsearch,kibana,logstash,nuclio,opa,${no_proxy} + NUMPROCS: 1 + command: -c supervisord/worker.webhooks.conf + volumes: + - cvat_data:/home/django/data + - cvat_keys:/home/django/keys + - cvat_logs:/home/django/logs + networks: + - cvat + cvat_ui: container_name: cvat_ui image: cvat/ui:${CVAT_VERSION:-dev} diff --git a/package.json b/package.json index aea72342b35b..753cb40d6b8a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "eslint-config-airbnb-base": "14.2.1", "eslint-config-airbnb-typescript": "^12.0.0", "eslint-plugin-cypress": "^2.11.2", - "eslint-plugin-header": "^3.1.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^26.5.3", "eslint-plugin-jsx-a11y": "^6.3.1", diff --git a/supervisord/all.conf b/supervisord/all.conf index 6d9b8ee2f42f..2c89006d702d 100644 --- a/supervisord/all.conf +++ b/supervisord/all.conf @@ -35,6 +35,12 @@ command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -i environment=SSH_AUTH_SOCK="/tmp/ssh-agent.sock" numprocs=1 +[program:rqworker_webhooks] +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic \ + "exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 webhooks" +environment=SSH_AUTH_SOCK="/tmp/ssh-agent.sock" +numprocs=1 + [program:git_status_updater] command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic \ "python3 ~/manage.py update_git_states" diff --git a/supervisord/worker.webhooks.conf b/supervisord/worker.webhooks.conf new file mode 100644 index 000000000000..67ca2745c60d --- /dev/null +++ b/supervisord/worker.webhooks.conf @@ -0,0 +1,36 @@ +[unix_http_server] +file = /tmp/supervisord/supervisor.sock + +[supervisorctl] +serverurl = unix:///tmp/supervisord/supervisor.sock + + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisord] +nodaemon=true +logfile=%(ENV_HOME)s/logs/supervisord.log ; supervisord log file +logfile_maxbytes=50MB ; maximum size of logfile before rotation +logfile_backups=10 ; number of backed up logfiles +loglevel=debug ; info, debug, warn, trace +pidfile=/tmp/supervisord/supervisord.pid ; pidfile location +childlogdir=%(ENV_HOME)s/logs/ ; where child log files will live + +[program:ssh-agent] +command=bash -c "rm /tmp/ssh-agent.sock -f && /usr/bin/ssh-agent -d -a /tmp/ssh-agent.sock" +priority=1 +autorestart=true + +[program:rqworker_webhooks] +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic \ + "exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 webhooks" +environment=SSH_AUTH_SOCK="/tmp/ssh-agent.sock" +numprocs=%(ENV_NUMPROCS)s + +[program:clamav_update] +command=bash -c "if [ \"${CLAM_AV}\" = 'yes' ]; then /usr/bin/freshclam -d \ + -l %(ENV_HOME)s/logs/freshclam.log --foreground=true; fi" +numprocs=1 + +environment=SSH_AUTH_SOCK="/tmp/ssh-agent.sock" diff --git a/tests/.eslintrc.js b/tests/.eslintrc.js index ae75108f494c..dc3af9584d8d 100644 --- a/tests/.eslintrc.js +++ b/tests/.eslintrc.js @@ -14,7 +14,7 @@ module.exports = { '.eslintrc.js', 'lint-staged.config.js', ], - plugins: ['security', 'no-unsanitized', 'eslint-plugin-header', 'import'], + plugins: ['security', 'no-unsanitized', 'import'], extends: [ 'eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM', 'plugin:cypress/recommended', 'airbnb-base', 'plugin:import/errors', 'plugin:import/warnings', diff --git a/tests/cypress.json b/tests/cypress.json index 6fb016472517..51ca3eee2284 100644 --- a/tests/cypress.json +++ b/tests/cypress.json @@ -13,6 +13,7 @@ "testFiles": [ "auth_page.js", "skeletons_pipeline.js", + "webhooks.js", "actions_tasks/**/*.js", "actions_tasks2/**/*.js", "actions_tasks3/**/*.js", diff --git a/tests/cypress/integration/webhooks.js b/tests/cypress/integration/webhooks.js new file mode 100644 index 000000000000..d1d64d9b42b8 --- /dev/null +++ b/tests/cypress/integration/webhooks.js @@ -0,0 +1,105 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +context('Webhooks pipeline.', () => { + const organizationParams = { + shortName: 'WebhooksOrg', + fullName: 'Organization full name. Only for test.', + description: 'This organization was created to test the functionality.', + email: 'testorganization@local.local', + phoneNumber: '+70000000000', + location: 'Country, State, Address, 000000', + }; + const orgWebhookParams = { + targetURL: 'https://localhost:3001/organization', + description: 'Sample description', + secret: 'Super secret', + enableSSL: true, + isActive: true, + events: [ + 'project', 'job', 'task', + ], + }; + const projectWebhookParams = { + targetURL: 'https://localhost:3001/project', + description: 'Sample description', + secret: 'Super secret', + enableSSL: true, + isActive: true, + }; + const newOrganizationWebhookParams = { + targetURL: 'https://localhost:3001/edited', + description: 'Edited description', + secret: 'Super secret', + enableSSL: true, + isActive: false, + events: [ + 'job', + ], + }; + + const project = { + name: 'Project for webhooks', + label: 'car', + attrName: 'color', + attrVaue: 'red', + multiAttrParams: false, + }; + + before(() => { + cy.visit('auth/login'); + cy.login(); + cy.createOrganization(organizationParams); + cy.activateOrganization(organizationParams.shortName); + cy.visit('/projects'); + cy.createProjects( + project.name, + project.label, + project.attrName, + project.attrVaue, + project.multiAttrParams, + ); + }); + + after(() => { + cy.logout(); + cy.getAuthKey().then((authKey) => { + cy.deleteProjects(authKey, [project.name]); + cy.deleteOrganizations(authKey, [organizationParams.shortName]); + }); + }); + + describe('Test organization webhook', () => { + it('Open the organization. Create/update/delete webhook.', () => { + cy.openOrganization(organizationParams.shortName); + cy.openOrganizationWebhooks(); + cy.createWebhook(orgWebhookParams); + cy.get('.cvat-webhooks-list').within(() => { + cy.contains(orgWebhookParams.description).should('exist'); + cy.contains(orgWebhookParams.targetURL).should('exist'); + }); + + cy.editWebhook(orgWebhookParams.description, newOrganizationWebhookParams); + cy.get('.cvat-webhooks-list').within(() => { + cy.contains(newOrganizationWebhookParams.description).should('exist'); + cy.contains(newOrganizationWebhookParams.targetURL).should('exist'); + }); + + cy.deleteWebhook(newOrganizationWebhookParams.description); + }); + }); + + describe('Test project webhook', () => { + it('Open the project. Create webhook.', () => { + cy.goToProjectsList(); + cy.openProject(project.name); + cy.openProjectWebhooks(); + cy.createWebhook(projectWebhookParams); + cy.get('.cvat-webhooks-list').within(() => { + cy.contains(projectWebhookParams.description).should('exist'); + cy.contains(projectWebhookParams.targetURL).should('exist'); + }); + }); + }); +}); diff --git a/tests/cypress/support/commands_webhooks.js b/tests/cypress/support/commands_webhooks.js new file mode 100644 index 000000000000..0d6a6ad25888 --- /dev/null +++ b/tests/cypress/support/commands_webhooks.js @@ -0,0 +1,81 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +Cypress.Commands.add('createWebhook', (webhookData) => { + cy.get('.cvat-create-webhook').click(); + cy.get('.cvat-setup-webhook-content').should('exist'); + cy.setUpWebhook(webhookData); + cy.get('.cvat-notification-create-webhook-success').should('exist').find('[data-icon="close"]').click(); + cy.get('.cvat-webhooks-go-back').click(); +}); + +Cypress.Commands.add('openWebhookActions', (description) => { + cy.contains(description).parents('.cvat-webhooks-list-item').within(() => { + cy.get('.cvat-webhooks-page-actions-button').trigger('mouseover'); + }); +}); + +Cypress.Commands.add('editWebhook', (description, webhookData) => { + cy.openWebhookActions(description); + cy.contains('[role="menuitem"]', 'Edit').click(); + cy.get('.cvat-setup-webhook-content').should('exist'); + cy.setUpWebhook(webhookData); + cy.get('.cvat-notification-update-webhook-success').should('exist').find('[data-icon="close"]').click(); + cy.get('.cvat-webhooks-go-back').click(); +}); + +Cypress.Commands.add('deleteWebhook', (description) => { + cy.openWebhookActions(description); + cy.contains('[role="menuitem"]', 'Delete').click(); + cy.get('.cvat-modal-confirm-remove-webhook') + .should('contain', 'Are you sure you want to remove the hook?') + .within(() => { + cy.contains('button', 'OK').click(); + }); + cy.contains(description).parents('.cvat-webhooks-list-item').should('have.css', 'opacity', '0.5'); +}); + +Cypress.Commands.add('setUpWebhook', (webhookData) => { + cy.get('#targetURL').clear().type(webhookData.targetURL); + cy.get('#description').clear().type(webhookData.description); + cy.get('#secret').clear().type(webhookData.secret); + if (!webhookData.enableSSL) cy.get('#enableSSL').uncheck(); + if (!webhookData.isActive) cy.get('#isActive').uncheck(); + + if (webhookData.events && Array.isArray(webhookData.events)) { + cy.get('#eventsMethod') + .within(() => { + cy.contains('Select individual events').click(); + }); + cy.get('.cvat-setup-webhook-content').within(() => { + cy.get('.cvat-webhook-detailed-events').within(() => { + cy.get('[type="checkbox"]').uncheck(); + for (const event of webhookData.events) { + cy.contains(event).click(); + } + }); + }); + } + cy.get('.cvat-setup-webhook-content').within(() => { + cy.contains('Submit').click(); + }); +}); + +Cypress.Commands.add('openOrganizationWebhooks', () => { + cy.get('.cvat-organization-page-actions-button').trigger('mouseover'); + cy.get('.cvat-organization-actions-menu').within(() => { + cy.contains('[role="menuitem"]', 'Setup webhooks').click(); + }); + cy.get('.cvat-spinner').should('not.exist'); + cy.get('.cvat-webhooks-page').should('exist'); +}); + +Cypress.Commands.add('openProjectWebhooks', () => { + cy.get('.cvat-project-page-actions-button').trigger('mouseover'); + cy.get('.cvat-project-actions-menu').within(() => { + cy.contains('[role="menuitem"]', 'Setup webhooks').click(); + }); + cy.get('.cvat-spinner').should('not.exist'); + cy.get('.cvat-webhooks-page').should('exist'); +}); diff --git a/tests/cypress/support/index.js b/tests/cypress/support/index.js index 8fc15b30144a..a7cc9b519dce 100644 --- a/tests/cypress/support/index.js +++ b/tests/cypress/support/index.js @@ -12,6 +12,7 @@ require('./commands_models'); require('./commands_opencv'); require('./commands_organizations'); require('./commands_cloud_storages'); +require('./commands_webhooks'); require('@cypress/code-coverage/support'); require('cypress-real-events/support'); diff --git a/tests/docker-compose.webhook.yml b/tests/docker-compose.webhook.yml new file mode 100644 index 000000000000..25f35c9e805f --- /dev/null +++ b/tests/docker-compose.webhook.yml @@ -0,0 +1,17 @@ +version: '3.3' + +services: + webhook_receiver: + image: python:3.9-slim + restart: always + command: python3 /tmp/server.py + env_file: + - ./tests/python/webhook_receiver/.env + expose: + - ${SERVER_PORT} + volumes: + - ./tests/python/webhook_receiver:/tmp + networks: + cvat: + aliases: + - webhooks diff --git a/tests/python/rest_api/test_projects.py b/tests/python/rest_api/test_projects.py index dfce180a8cd3..2966a37aec47 100644 --- a/tests/python/rest_api/test_projects.py +++ b/tests/python/rest_api/test_projects.py @@ -20,14 +20,9 @@ @pytest.mark.usefixtures('dontchangedb') class TestGetProjects: def _find_project_by_user_org(self, user, projects, is_project_staff_flag, is_project_staff): - if is_project_staff_flag: - for p in projects: - if is_project_staff(user['id'], p['id']): - return p['id'] - else: - for p in projects: - if not is_project_staff(user['id'], p['id']): - return p['id'] + for p in projects: + if is_project_staff(user['id'], p['id']) == is_project_staff_flag: + return p['id'] def _test_response_200(self, username, project_id, **kwargs): with make_api_client(username) as api_client: @@ -78,48 +73,43 @@ def test_user_cannot_see_project(self, projects, find_users, is_project_staff, o ) self._test_response_403(user['username'], project['id']) - # Member of organization that has role supervisor or worker cannot see - # project if this member not in [project:owner, project:assignee] @pytest.mark.parametrize('role', ('supervisor', 'worker')) def test_if_supervisor_or_worker_cannot_see_project(self, projects, is_project_staff, find_users, role): - non_admins = find_users(role=role, exclude_privilege='admin') - assert non_admins is not None - - project_id = self._find_project_by_user_org(non_admins[0], projects, False, is_project_staff) - assert project_id is not None + user, pid = next(( + (user, project['id']) + for user in find_users(role=role, exclude_privilege='admin') + for project in projects + if project['organization'] == user['org'] \ + and not is_project_staff(user['id'], project['id']) + )) - self._test_response_403(non_admins[0]['username'], project_id) + self._test_response_403(user['username'], pid) - # Member of organization that has role maintainer or owner can see any - # project even this member not in [project:owner, project:assignee] @pytest.mark.parametrize('role', ('maintainer', 'owner')) def test_if_maintainer_or_owner_can_see_project(self, find_users, projects, is_project_staff, role): - non_admins = find_users(role=role, exclude_privilege='admin') - assert non_admins is not None + user, pid = next(( + (user, project['id']) + for user in find_users(role=role, exclude_privilege='admin') + for project in projects + if project['organization'] == user['org'] \ + and not is_project_staff(user['id'], project['id']) + )) - project_id = self._find_project_by_user_org(non_admins[0], projects, False, is_project_staff) - assert project_id is not None + self._test_response_200(user['username'], pid, org_id=user['org']) - self._test_response_200(non_admins[0]['username'], project_id, org_id=non_admins[0]['org']) - - # Member of organization that has role supervisor or worker can see - # project if this member in [project:owner, project:assignee] @pytest.mark.parametrize('role', ('supervisor', 'worker')) def test_if_org_member_supervisor_or_worker_can_see_project(self, projects, find_users, is_project_staff, role): - non_admins = find_users(role=role, exclude_privilege='admin') - assert len(non_admins) - - for u in non_admins: - project_id = self._find_project_by_user_org(u, projects, True, is_project_staff) - if project_id: - user_in_project = u - break - - assert project_id is not None + user, pid = next(( + (user, project['id']) + for user in find_users(role=role, exclude_privilege='admin') + for project in projects + if project['organization'] == user['org'] \ + and is_project_staff(user['id'], project['id']) + )) - self._test_response_200(user_in_project['username'], project_id, org_id=user_in_project['org']) + self._test_response_200(user['username'], pid, org_id=user['org']) class TestGetProjectBackup: def _test_can_get_project_backup(self, username, pid, **kwargs): @@ -159,7 +149,9 @@ def test_org_worker_cannot_get_project_backup(self, find_users, projects, is_pro user, project = next( (user, project) for user, project in product(users, projects) - if not is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if not is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) self._test_cannot_get_project_backup(user['username'], project['id'], org_id=project['organization']) @@ -171,7 +163,9 @@ def test_org_worker_can_get_project_backup(self, find_users, projects, is_projec user, project = next( (user, project) for user, project in product(users, projects) - if is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) self._test_can_get_project_backup(user['username'], project['id'], org_id=project['organization']) @@ -183,7 +177,9 @@ def test_org_supervisor_can_get_project_backup(self, find_users, projects, is_pr user, project = next( (user, project) for user, project in product(users, projects) - if is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) self._test_can_get_project_backup(user['username'], project['id'], org_id=project['organization']) @@ -195,7 +191,9 @@ def test_org_supervisor_cannot_get_project_backup(self, find_users, projects, is user, project = next( (user, project) for user, project in product(users, projects) - if not is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if not is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) self._test_cannot_get_project_backup(user['username'], project['id'], org_id=project['organization']) @@ -207,7 +205,9 @@ def test_org_maintainer_can_get_project_backup(self, find_users, projects, is_pr user, project = next( (user, project) for user, project in product(users, projects) - if not is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if not is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) self._test_can_get_project_backup(user['username'], project['id'], org_id=project['organization']) @@ -219,7 +219,9 @@ def test_org_owner_can_get_project_backup(self, find_users, projects, is_project user, project = next( (user, project) for user, project in product(users, projects) - if not is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if not is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) self._test_can_get_project_backup(user['username'], project['id'], org_id=project['organization']) @@ -248,7 +250,7 @@ def test_if_worker_cannot_create_project(self, find_users): self._test_create_project_403(username, spec) @pytest.mark.parametrize('privilege', ('admin', 'business', 'user')) - def test_is_user_can_create_project(self, find_users, privilege): + def test_if_user_can_create_project(self, find_users, privilege): privileged_users = find_users(privilege=privilege) assert len(privileged_users) @@ -427,7 +429,7 @@ def test_admin_can_delete_label(self, projects): assert len(response.json()['labels']) == len(project['labels']) - 1 def test_admin_can_delete_skeleton_label(self, projects): - project = deepcopy(list(projects)[0]) + project = deepcopy(projects[5]) labels = project['labels'][0] labels.update({'deleted': True}) response = patch_method('admin1', f'/projects/{project["id"]}', {'labels': [labels]}) @@ -456,7 +458,9 @@ def test_org_maintainer_can_add_label(self, find_users, projects, is_project_sta user, project = next( (user, project) for user, project in product(users, projects) - if not is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if not is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) labels = {'name': 'new name'} @@ -471,7 +475,9 @@ def test_org_supervisor_can_add_label(self, find_users, projects, is_project_sta user, project = next( (user, project) for user, project in product(users, projects) - if not is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if not is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) labels = {'name': 'new name'} @@ -485,7 +491,9 @@ def test_org_worker_cannot_add_label(self, find_users, projects, is_project_staf user, project = next( (user, project) for user, project in product(users, projects) - if not is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if not is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) labels = {'name': 'new name'} @@ -499,7 +507,9 @@ def test_org_worker_can_add_label(self, find_users, projects, is_project_staff, user, project = next( (user, project) for user, project in product(users, projects) - if is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) labels = {'name': 'new name'} @@ -514,7 +524,9 @@ def test_org_owner_can_add_label(self, find_users, projects, is_project_staff, i user, project = next( (user, project) for user, project in product(users, projects) - if not is_project_staff(user['id'], project['id']) and is_org_member(user['id'], project['organization']) + if not is_project_staff(user['id'], project['id']) + and project['organization'] + and is_org_member(user['id'], project['organization']) ) labels = {'name': 'new name'} diff --git a/tests/python/rest_api/test_webhooks.py b/tests/python/rest_api/test_webhooks.py new file mode 100644 index 000000000000..34f193f4f381 --- /dev/null +++ b/tests/python/rest_api/test_webhooks.py @@ -0,0 +1,1052 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from copy import deepcopy +from http import HTTPStatus +from itertools import product + +import pytest +from deepdiff import DeepDiff + +from shared.utils.config import delete_method, get_method, patch_method, post_method + + +@pytest.mark.usefixtures('changedb') +class TestPostWebhooks: + proj_webhook = { + 'description': 'webhook description', + 'content_type': 'application/json', + 'enable_ssl': False, + 'events': [ + 'create:task', + 'delete:task' + ], + 'is_active': True, + 'project_id': 1, + 'secret': 'secret', + 'target_url': 'http://example.com', + 'type': 'project', + } + + org_webhook = { + 'description': 'webhook description', + 'content_type': 'application/json', + 'enable_ssl': False, + 'events': [ + 'create:task', + 'delete:task' + ], + 'is_active': True, + 'secret': 'secret', + 'target_url': 'http://example.com', + 'type': 'organization', + } + + def test_sandbox_admin_can_create_webhook_for_project(self, projects, users): + admin = next((u for u in users if 'admin' in u['groups'])) + project = [p for p in projects if p['owner']['id'] != admin['id'] and p['organization'] is None][0] + + webhook = deepcopy(self.proj_webhook) + webhook['project_id'] = project['id'] + + response = post_method(admin['username'], 'webhooks', webhook) + + assert response.status_code == HTTPStatus.CREATED + assert 'secret' not in response.json() + + def test_admin_can_create_webhook_for_org(self, users, organizations, is_org_member): + admins = [u for u in users if 'admin' in u['groups']] + username, org_id = next(( + (user['username'], org['id']) + for user in admins + for org in organizations + if not is_org_member(user['id'], org['id']) + )) + + webhook = deepcopy(self.org_webhook) + + response = post_method(username, 'webhooks', webhook, org_id=org_id) + + assert response.status_code == HTTPStatus.CREATED + assert 'secret' not in response.json() + + def test_admin_can_create_webhook_for_project_in_org(self, users, projects_by_org, + organizations, is_org_member): + admins = [u for u in users if 'admin' in u['groups']] + not_org_members = [(u, o) for u, o in product(admins, organizations) + if not is_org_member(u['id'], o['id'])] + + username, org_id = next(( + (u['username'], o['id']) + for u, o in not_org_members + for p in projects_by_org.get(o['id'], []) + if p['owner']['id'] != u['id'] + )) + + webhook = deepcopy(self.org_webhook) + + response = post_method(username, 'webhooks', webhook, org_id=org_id) + + assert response.status_code == HTTPStatus.CREATED + assert 'secret' not in response.json() + + + @pytest.mark.parametrize('privilege', ['user', 'business']) + def test_sandbox_project_owner_can_create_webhook_for_project(self, privilege, projects, users): + users = [user for user in users if privilege in user['groups']] + username, project_id = next(( + (user['username'], project['id']) + for user in users + for project in projects + if project['owner']['id'] == user['id'] and project['organization'] is None + )) + + webhook = deepcopy(self.proj_webhook) + webhook['project_id'] = project_id + + response = post_method(username, 'webhooks', webhook) + + assert response.status_code == HTTPStatus.CREATED + assert 'secret' not in response.json() + + @pytest.mark.parametrize('privilege', ['worker', 'user', 'business']) + def test_sandbox_project_assignee_cannot_create_webhook_for_project(self, privilege, projects, users): + users = [u for u in users if privilege in u['groups']] + projects = [p for p in projects if p['assignee'] is not None] + username, project_id = next(( + (user['username'], project['id']) + for user in users + for project in projects + if project['assignee']['id'] == user['id'] and project['organization'] is None + )) + + + webhook = deepcopy(self.proj_webhook) + webhook['project_id'] = project_id + + response = post_method(username, 'webhooks', webhook) + + assert response.status_code == HTTPStatus.FORBIDDEN + + @pytest.mark.parametrize('role', ['maintainer', 'owner']) + def test_member_can_create_webhook_for_org(self, role, find_users, organizations): + username, org_id = next(( + (u['username'], o['id']) + for o in organizations + for u in find_users(org=o['id'], role=role, exclude_privilege='admin') + )) + + webhook = deepcopy(self.org_webhook) + + response = post_method(username, 'webhooks', webhook, org_id=org_id) + + assert response.status_code == HTTPStatus.CREATED + assert 'secret' not in response.json() + + @pytest.mark.parametrize('role', ['maintainer', 'owner']) + def test_member_can_create_webhook_for_project(self, role, find_users, organizations, + projects_by_org, is_project_staff): + username, oid, pid = next(( + (u['username'], o['id'], p['id']) + for o in organizations + for u in find_users(org=o['id'], role=role, exclude_privilege='admin') + for p in projects_by_org.get(o['id'], []) + if not is_project_staff(u['id'], p['id']) + )) + + webhook = deepcopy(self.proj_webhook) + webhook['project_id'] = pid + + response = post_method(username, 'webhooks', webhook, org_id=oid) + + assert response.status_code == HTTPStatus.CREATED + assert 'secret' not in response.json() + + @pytest.mark.parametrize('role', ['supervisor', 'worker']) + def test_member_cannot_create_webhook_for_org(self, role, find_users, organizations): + username, org_id = next(( + (u['username'], o['id']) + for o in organizations + for u in find_users(org=o['id'], role=role, exclude_privilege='admin') + )) + + webhook = deepcopy(self.org_webhook) + + response = post_method(username, 'webhooks', webhook, org_id=org_id) + + assert response.status_code == HTTPStatus.FORBIDDEN + + @pytest.mark.parametrize('role', ['supervisor', 'worker']) + def test_member_cannot_create_webhook_for_project(self, role, find_users, organizations, + projects_by_org, is_project_staff): + username, oid, pid = next(( + (u['username'], o['id'], p['id']) + for o in organizations + for u in find_users(org=o['id'], role=role, exclude_privilege='admin') + for p in projects_by_org.get(o['id'], []) + if not is_project_staff(u['id'], p['id']) + )) + + webhook = deepcopy(self.proj_webhook) + webhook['project_id'] = pid + + response = post_method(username, 'webhooks', webhook, org_id=oid) + + assert response.status_code == HTTPStatus.FORBIDDEN + + @pytest.mark.parametrize('role', ['supervisor']) + def test_member_project_owner_can_create_webhook_for_project(self, role, find_users, organizations, + projects_by_org, is_project_staff): + username, oid, pid = next(( + (u['username'], o['id'], p['id']) + for o in organizations + for u in find_users(org=o['id'], role=role, exclude_privilege='admin') + for p in projects_by_org.get(o['id'], []) + if p['owner']['id'] == u['id'] + )) + + webhook = deepcopy(self.proj_webhook) + webhook['project_id'] = pid + + response = post_method(username, 'webhooks', webhook, org_id=oid) + + assert response.status_code == HTTPStatus.CREATED + assert 'secret' not in response.json() + + def test_non_member_cannot_create_webhook_for_org(self, find_users, organizations, + is_org_member): + username, org_id = next(( + (u['username'], o['id']) + for o in organizations + for u in find_users(exclude_privilege='admin') + if not is_org_member(u['id'], o['id']) + )) + + webhook = deepcopy(self.org_webhook) + + response = post_method(username, 'webhooks', webhook, org_id=org_id) + + assert response.status_code == HTTPStatus.FORBIDDEN + + + def test_can_create_without_unnecessary_fields(self): + post_data = deepcopy(self.proj_webhook) + post_data.pop('enable_ssl') + post_data.pop('content_type') + post_data.pop('description') + post_data.pop('is_active') + post_data.pop('secret') + + response = post_method('admin2', 'webhooks', post_data) + + assert response.status_code == HTTPStatus.CREATED + + def test_cannot_create_without_target_url(self): + post_data = deepcopy(self.proj_webhook) + post_data.pop('target_url') + + response = post_method('admin2', 'webhooks', post_data) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + + def test_cannot_create_without_events_list(self): + post_data = deepcopy(self.proj_webhook) + post_data.pop('events') + + response = post_method('admin2', 'webhooks', post_data) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + def test_cannot_create_without_type(self): + post_data = deepcopy(self.proj_webhook) + post_data.pop('type') + + response = post_method('admin2', 'webhooks', post_data) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + def test_cannot_create_without_project_id(self): + post_data = deepcopy(self.proj_webhook) + post_data.pop('project_id') + + response = post_method('admin2', 'webhooks', post_data) + + assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + def test_cannot_create_organization_webhook_when_project_id_is_not_null(self, organizations): + post_data = deepcopy(self.proj_webhook) + post_data['type'] = 'organization' + org_id = organizations.raw[0]['id'] + + response = post_method('admin2', 'webhooks', post_data, org_id=org_id) + + assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + def test_cannot_create_non_unique_webhook(self): + pytest.skip('Not implemeted yet') + response = post_method('admin2', 'webhooks', self.proj_webhook) + + response = post_method('admin2', 'webhooks', self.proj_webhook) + + assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + def test_cannot_create_for_non_existent_organization(self, organizations): + post_data = deepcopy(self.proj_webhook) + post_data['type'] = 'organization' + org_id = max(a['id'] for a in organizations.raw) + 1 + + response = post_method('admin2', 'webhooks', post_data, org_id=org_id) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + def test_cannot_create_for_non_existent_project(self, projects): + post_data = deepcopy(self.proj_webhook) + post_data['project_id'] = max(a['id'] for a in projects.raw) + 1 + + response = post_method('admin2', 'webhooks', post_data) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + + def test_cannot_create_with_non_supported_type(self): + post_data = deepcopy(self.proj_webhook) + post_data['type'] = 'some_type' + + response = post_method('admin2', 'webhooks', post_data) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + def test_cannot_create_with_non_supported_content_type(self): + post_data = deepcopy(self.proj_webhook) + post_data['content_type'] = ['application/x-www-form-urlencoded'] + + response = post_method('admin2', 'webhooks', post_data) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + @pytest.mark.parametrize('event', ['some:event', 'create:project', 'update:organization', 'create:invitation']) + def test_cannot_create_project_webhook_with_non_supported_event_type(self, event): + post_data = deepcopy(self.proj_webhook) + post_data['events'] = [event] + + response = post_method('admin2', 'webhooks', post_data) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + @pytest.mark.parametrize('event', ['some:event', 'create:organization']) + def test_cannot_create_organization_webhook_with_non_supported_event_type(self, event, organizations): + post_data = deepcopy(self.proj_webhook) + post_data['type'] = 'organization' + post_data['events'] = [event] + org_id = next(iter(organizations))['id'] + + response = post_method('admin2', 'webhooks', post_data, org_id=org_id) + + assert response.status_code == HTTPStatus.BAD_REQUEST + +@pytest.mark.usefixtures('dontchangedb') +class TestGetWebhooks: + def test_admin_can_get_webhook(self, webhooks, users, projects): + proj_webhooks = [w for w in webhooks if w['type'] == 'project'] + username, wid = next(( + (user['username'], webhook['id']) + for user in users + for webhook in proj_webhooks + if 'admin' in user['groups'] + and webhook['owner']['id'] != user['id'] + and projects[webhook['project']]['owner']['id'] != user['id'] + )) + + response = get_method(username, f"webhooks/{wid}") + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhooks[wid], response.json(), ignore_order=True) == {} + + @pytest.mark.parametrize('privilege', ['user', 'business']) + def test_project_owner_can_get_webhook(self, privilege, webhooks, projects, users): + proj_webhooks = [w for w in webhooks if w['type'] == 'project'] + username, wid = next(( + (user['username'], webhook['id']) + for user in users + for webhook in proj_webhooks + if privilege not in user['groups'] + and projects[webhook['project']]['owner']['id'] == user['id'] + )) + + response = get_method(username, f"webhooks/{wid}") + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhooks[wid], response.json(), ignore_order=True) == {} + + @pytest.mark.parametrize('privilege', ['user', 'business']) + def test_webhook_owner_can_get_webhook(self, privilege, webhooks, projects, users): + proj_webhooks = [w for w in webhooks if w['type'] == 'project'] + username, wid = next(( + (user['username'], webhook['id']) + for user in users + for webhook in proj_webhooks + if privilege in user['groups'] + and webhook['owner']['id'] == user['id'] + )) + + response = get_method(username, f"webhooks/{wid}") + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhooks[wid], response.json(), ignore_order=True) == {} + + @pytest.mark.parametrize('privilege', ['user', 'business']) + def test_not_project_staff_cannot_get_webhook(self, privilege, webhooks, + projects, users): + proj_webhooks = [w for w in webhooks if w['type'] == 'project'] + username, wid = next(( + (user['username'], webhook['id']) + for user in users + for webhook in proj_webhooks + if privilege in user['groups'] + and projects[webhook['project']]['owner']['id'] != user['id'] + and webhook['owner']['id'] != user['id'] + )) + + response = get_method(username, f"webhooks/{wid}") + + assert response.status_code == HTTPStatus.FORBIDDEN + + @pytest.mark.parametrize('role', ['owner', 'maintainer']) + def test_org_staff_can_see_org_webhook(self, role, webhooks, find_users): + webhook = next((w for w in webhooks if w['type'] == 'organization')) + username = next(( + u['username'] for u in find_users(role=role, org=webhook['organization']) + )) + + response = get_method(username, f"webhooks/{webhook['id']}", + org_id=webhook['organization']) + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhook, response.json(), ignore_order=True) == {} + + @pytest.mark.parametrize('role', ['owner', 'maintainer']) + def test_org_staff_can_see_project_webhook_in_org(self, role, webhooks, find_users, + projects): + proj_webhooks = [w for w in webhooks if w['organization'] is not None and + w['type'] == 'project'] + username, webhook = next(( + (user['username'], webhook) + for webhook in proj_webhooks + for user in find_users(role=role, org=webhook['organization']) + if projects[webhook['project']]['owner']['id'] != user['id'] + and webhook['owner']['id'] != user['id'] + )) + + response = get_method(username, f"webhooks/{webhook['id']}", + org_id=webhook['organization']) + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhook, response.json(), ignore_order=True) == {} + + @pytest.mark.parametrize('role', ['worker', 'supervisor']) + def test_member_cannot_get_org_webhook(self, role, webhooks, find_users): + webhook = next((w for w in webhooks if w['type'] == 'organization')) + username = next(( + u['username'] for u in find_users(role=role, org=webhook['organization']) + )) + + response = get_method(username, f"webhooks/{webhook['id']}", + org_id=webhook['organization']) + + assert response.status_code == HTTPStatus.FORBIDDEN + + @pytest.mark.parametrize('role', ['worker', 'supervisor']) + def test_member_cannot_get_project_webhook_in_org(self, role, webhooks, find_users, + projects): + proj_webhooks = [w for w in webhooks if w['organization'] is not None and + w['type'] == 'project'] + username, webhook = next(( + (user['username'], webhook) + for webhook in proj_webhooks + for user in find_users(role=role, org=webhook['organization']) + if projects[webhook['project']]['owner']['id'] != user['id'] + and webhook['owner']['id'] != user['id'] + )) + + response = get_method(username, f"webhooks/{webhook['id']}", + org_id=webhook['organization']) + + assert response.status_code == HTTPStatus.FORBIDDEN + + @pytest.mark.parametrize('role', ['supervisor']) + def test_member_can_get_project_webhook_in_org(self, role, webhooks, find_users, + projects): + proj_webhooks = [w for w in webhooks if w['organization'] is not None and + w['type'] == 'project'] + username, webhook = next(( + (user['username'], webhook) + for webhook in proj_webhooks + for user in find_users(role=role, org=webhook['organization']) + if projects[webhook['project']]['owner']['id'] == user['id'] + or webhook['owner']['id'] == user['id'] + )) + + response = get_method(username, f"webhooks/{webhook['id']}", + org_id=webhook['organization']) + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhook, response.json(), ignore_order=True) == {} + + +@pytest.mark.usefixtures('dontchangedb') +class TestGetListWebhooks: + def test_can_get_webhooks_list(self, webhooks): + response = get_method('admin2', 'webhooks') + + assert response.status_code == HTTPStatus.OK + assert all(['secret' not in webhook for webhook in response.json()['results']]) + assert DeepDiff(webhooks.raw, response.json()['results'], ignore_order=True) == {} + + def test_admin_can_get_webhooks_for_project(self, webhooks): + pid = next((webhook['project'] for webhook in webhooks + if webhook['type'] == 'project' and webhook['organization'] is None)) + + expected_response = [webhook for webhook in webhooks + if webhook['type'] == 'project' and webhook['project'] == pid] + filter_val = '{"and":[{"==":[{"var":"project_id"},%s]}]}' % pid + + response = get_method('admin2', "webhooks", filter=filter_val) + + assert response.status_code == HTTPStatus.OK + assert DeepDiff(expected_response, response.json()['results'], ignore_order=True) == {} + + def test_admin_can_get_webhooks_for_organization(self, webhooks): + org_id = next((webhook['organization'] for webhook in webhooks + if webhook['organization'] is not None)) + + expected_response = [webhook for webhook in webhooks + if webhook['organization'] == org_id] + + response = get_method('admin2', "webhooks", org_id=org_id) + + assert response.status_code == HTTPStatus.OK + assert DeepDiff(expected_response, response.json()['results'], ignore_order=True) == {} + + def test_admin_can_get_webhooks_for_project_in_org(self, webhooks): + pid, oid = next(( + (webhook['project'], webhook['organization']) + for webhook in webhooks + if webhook['type'] == 'project' + and webhook['organization'] is not None + )) + + expected_response = [webhook for webhook in webhooks + if webhook['project'] == pid and webhook['organization'] == oid] + filter_val = '{"and":[{"==":[{"var":"project_id"},%s]}]}' % pid + + response = get_method('admin2', "webhooks", org_id=oid, filter=filter_val) + + assert response.status_code == HTTPStatus.OK + assert DeepDiff(expected_response, response.json()['results'], ignore_order=True) == {} + + @pytest.mark.parametrize('privilege', ['user', 'business']) + def test_user_cannot_get_webhook_list_for_project(self, privilege, find_users, + webhooks, projects): + username, pid = next(( + (user['username'], webhook['project']) + for user in find_users(privilege=privilege) + for webhook in webhooks + if webhook['type'] == 'project' + and webhook['organization'] is None + and webhook['owner']['id'] != user['id'] + and projects[webhook['project']]['owner']['id'] != user['id'] + )) + + filter_val = '{"and":[{"==":[{"var":"project_id"},%s]}]}' % pid + + response = get_method(username, "webhooks", filter=filter_val) + + assert response.status_code == HTTPStatus.OK + assert DeepDiff([], response.json()['results'], ignore_order=True) == {} + + @pytest.mark.parametrize('privilege', ['user', 'business']) + def test_user_can_get_webhook_list_for_project(self, privilege, find_users, + webhooks, projects): + username, pid = next(( + (user['username'], webhook['project']) + for user in find_users(privilege=privilege) + for webhook in webhooks + if webhook['type'] == 'project' + and webhook['organization'] is None + and projects[webhook['project']]['owner']['id'] == user['id'] + )) + + expected_response = [w for w in webhooks + if w['type'] == 'project' and w['project'] == pid] + filter_val = '{"and":[{"==":[{"var":"project_id"},%s]}]}' % pid + + response = get_method(username, "webhooks", filter=filter_val) + + assert response.status_code == HTTPStatus.OK + assert DeepDiff(expected_response, response.json()['results'], + ignore_order=True) == {} + + def test_non_member_cannot_see_webhook_list_for_org(self, webhooks, users, + is_org_member): + username, org_id = next(( + (user['username'], webhook['organization']) + for webhook in webhooks + for user in users + if webhook['organization'] is not None + and not is_org_member(user['id'], webhook['organization']) + and 'admin' not in user['groups'] + )) + + response = get_method(username, 'webhooks', org_id=org_id) + + assert response.status_code == HTTPStatus.FORBIDDEN + + @pytest.mark.parametrize('role', ['maintainer', 'owner']) + def test_org_staff_can_see_all_org_webhooks(self, role, webhooks, organizations, + find_users): + username, org_id = next(( + (user['username'], org['id']) + for webhook in webhooks + for org in organizations + for user in find_users(role=role, org=org['id']) + if webhook['organization'] == org['id'] + )) + + expected_response = [webhook for webhook in webhooks + if webhook['organization'] == org_id] + + response = get_method(username, 'webhooks', org_id=org_id) + + assert response.status_code == HTTPStatus.OK + assert DeepDiff(expected_response, response.json()['results'], + ignore_order=True) == {} + + @pytest.mark.parametrize('role', ['worker', 'supervisor']) + def test_member_cannot_see_all_org_webhook(self, role, webhooks, organizations, + find_users, projects): + username, org_id = next(( + (user['username'], org['id']) + for webhook in webhooks + for org in organizations + for user in find_users(role=role, org=org['id']) + if webhook['organization'] == org['id'] + )) + + expected_response = [ + webhook for webhook in webhooks + if webhook['organization'] == org_id + and (webhook['owner']['username'] == username + or (webhook['project'] + and projects[webhook['project']]['owner']['username'] == username)) + ] + + response = get_method(username, 'webhooks', org_id=org_id) + + assert response.status_code == HTTPStatus.OK + assert DeepDiff(expected_response, response.json()['results'], + ignore_order=True) == {} + + @pytest.mark.parametrize('role', ['supervisor']) + def test_member_can_see_list_of_project_webhooks_in_org(self, role, webhooks, + organizations, find_users, projects): + username, org_id = next(( + (user['username'], org['id']) + for webhook in webhooks + for org in organizations + for user in find_users(role=role, org=org['id']) + if webhook['organization'] == org['id'] + and webhook['type'] == 'project' + and projects[webhook['project']]['owner']['id'] == user['id'] + )) + + expected_response = [ + webhook for webhook in webhooks + if webhook['organization'] == org_id + and webhook['type'] == 'project' + and projects[webhook['project']]['owner']['username'] == username + ] + + response = get_method(username, 'webhooks', org_id=org_id) + + assert response.status_code == HTTPStatus.OK + assert DeepDiff(expected_response, response.json()['results'], + ignore_order=True) == {} + + +@pytest.mark.usefixtures('changedb') +class TestPatchWebhooks: + WID = 2 + + def test_sandbox_admin_can_update_any_webhook(self, webhooks, find_users): + username, webhook = next(( + (user['username'], deepcopy(webhook)) + for user in find_users(privilege='admin') + for webhook in webhooks + if webhook['owner']['id'] != user['id'] and webhook['organization'] is None + )) + patch_data = { + 'target_url': 'http://newexample.com', + 'secret': 'newsecret', + 'events': ['create:task'], + 'is_active': not webhook['is_active'], + 'enable_ssl': not webhook['enable_ssl'], + } + webhook.update(patch_data) + + response = patch_method(username, f"webhooks/{webhook['id']}", patch_data) + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhook, response.json(), ignore_order=True, + exclude_paths=["root['updated_date']", "root['secret']"]) == {} + + def test_cannot_update_with_nonexistent_contenttype(self): + patch_data = { + 'content_type': 'application/x-www-form-urlencoded', + } + + response = patch_method('admin2', f'webhooks/{self.WID}', patch_data) + assert response.status_code == HTTPStatus.BAD_REQUEST + + @pytest.mark.parametrize('privilege', ['user', 'business']) + def test_sandbox_user_can_update_webhook(self, privilege, find_users, webhooks): + username, webhook = next(( + (user['username'], deepcopy(webhook)) + for user in find_users(privilege=privilege) + for webhook in webhooks + if webhook['owner']['id'] == user['id'] + )) + + patch_data = {'target_url': 'http://newexample.com'} + webhook.update(patch_data) + + response = patch_method(username, f"webhooks/{webhook['id']}", patch_data) + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhook, response.json(), ignore_order=True, + exclude_paths=["root['updated_date']", "root['secret']"]) == {} + + + @pytest.mark.parametrize('privilege', ['worker', 'user', 'business']) + def test_sandbox_user_cannot_update_webhook(self, privilege, find_users, webhooks): + username, webhook = next(( + (user['username'], deepcopy(webhook)) + for user in find_users(privilege=privilege) + for webhook in webhooks + if webhook['owner']['id'] != user['id'] + )) + + patch_data = {'target_url': 'http://newexample.com'} + webhook.update(patch_data) + + response = patch_method(username, f"webhooks/{webhook['id']}", patch_data) + + assert response.status_code == HTTPStatus.FORBIDDEN + + def test_admin_can_update_org_webhook(self, find_users, organizations, webhooks, is_org_member): + org_webhooks = [w for w in webhooks if w['type'] == 'organization'] + admin, oid, webhook = next(( + (u['username'], o['id'], deepcopy(w)) + for u in find_users(privilege='admin') + for o in organizations + for w in org_webhooks + if w['organization'] == o['id'] and not is_org_member(u['id'], o['id']) + )) + + patch_data = {'target_url': 'http://newexample.com'} + webhook.update(patch_data) + + response = patch_method(admin, f"webhooks/{webhook['id']}", patch_data, org_id=oid) + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhook, response.json(), ignore_order=True, + exclude_paths=["root['updated_date']", "root['secret']"]) == {} + + @pytest.mark.parametrize('role', ['maintainer', 'owner']) + def test_member_can_update_org_webhook(self, role, find_users, organizations, webhooks): + org_webhooks = [w for w in webhooks if w['type'] == 'organization'] + username, oid, webhook = next(( + (u['username'], o['id'], deepcopy(w)) + for o in organizations + for u in find_users(role=role, org=o['id']) + for w in org_webhooks + if w['organization'] == o['id'] + )) + + patch_data = {'target_url': 'http://newexample.com'} + webhook.update(patch_data) + + response = patch_method(username, f"webhooks/{webhook['id']}", patch_data, org_id=oid) + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhook, response.json(), ignore_order=True, + exclude_paths=["root['updated_date']", "root['secret']"]) == {} + + + @pytest.mark.parametrize('role', ['worker', 'supervisor']) + def test_member_cannot_update_org_webhook(self, role, find_users, organizations, webhooks): + org_webhooks = [w for w in webhooks if w['type'] == 'organization'] + username, oid, webhook = next(( + (u['username'], o['id'], deepcopy(w)) + for o in organizations + for u in find_users(role=role, org=o['id']) + for w in org_webhooks + if w['organization'] == o['id'] + )) + + patch_data = {'target_url': 'http://newexample.com'} + webhook.update(patch_data) + + response = patch_method(username, f"webhooks/{webhook['id']}", patch_data, org_id=oid) + + assert response.status_code == HTTPStatus.FORBIDDEN + + @pytest.mark.parametrize('role, allow', [ + ('maintainer', True), ('owner', True), + ('supervisor', False), ('worker', False) + ]) + def test_member_can_update_any_project_webhook_in_org(self, role, allow, find_users, + organizations, projects_by_org, webhooks, is_project_staff): + proj_webhooks = [w for w in webhooks if w['type'] == 'project'] + username, org_id, webhook = next(( + (u['username'], o['id'], deepcopy(w)) + for o in organizations + for u in find_users(role=role, org=o['id']) + for w in proj_webhooks + for p in projects_by_org.get(o['id'], []) + if w['project'] == p['id'] + and w['organization'] == o['id'] + and not is_project_staff(u['id'], p['id']) + and w['owner']['id'] != u['id'] + )) + + patch_data = {'target_url': 'http://newexample.com'} + webhook.update(patch_data) + + response = patch_method(username, f"webhooks/{webhook['id']}", + patch_data, org_id=org_id) + + if not allow: + assert response.status_code == HTTPStatus.FORBIDDEN + else: + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhook, response.json(), ignore_order=True, + exclude_paths=["root['updated_date']", "root['secret']"]) == {} + + @pytest.mark.parametrize('role', ['supervisor']) + def test_member_can_update_project_webhook_in_org(self, role, find_users, + organizations, projects_by_org, webhooks): + proj_webhooks = [w for w in webhooks if w['type'] == 'project'] + username, org_id, webhook = next(( + (u['username'], o['id'], deepcopy(w)) + for o in organizations + for u in find_users(role=role, org=o['id']) + for w in proj_webhooks + for p in projects_by_org.get(o['id'], []) + if w['project'] == p['id'] + and w['organization'] == o['id'] + and u['id'] == p['owner']['id'] + )) + + patch_data = {'target_url': 'http://newexample.com'} + webhook.update(patch_data) + + response = patch_method(username, f"webhooks/{webhook['id']}", + patch_data, org_id=org_id) + + assert response.status_code == HTTPStatus.OK + assert 'secret' not in response.json() + assert DeepDiff(webhook, response.json(), ignore_order=True, + exclude_paths=["root['updated_date']", "root['secret']"]) == {} + + + +@pytest.mark.usefixtures('changedb') +class TestDeleteWebhooks: + @pytest.mark.parametrize('privilege, allow', [ + ('user', False), ('business', False), ('admin', True) + ]) + def test_user_can_delete_project_webhook(self, privilege, allow, find_users, + webhooks, projects): + users = find_users(privilege=privilege) + username, webhook_id = next(( + (user['username'], webhook['id']) + for webhook in webhooks for user in users + if webhook['type'] == 'project' + and webhook['organization'] is None + and webhook['owner']['id'] != user['id'] + and projects[webhook['project']]['owner']['id'] != user['id'] + )) + + if not allow: + response = delete_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.FORBIDDEN + else: + response = delete_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NO_CONTENT + + response = get_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NOT_FOUND + + def test_admin_can_delete_project_webhook_in_org(self, find_users, webhooks, + projects, is_org_member): + admins = find_users(privilege='admin') + username, webhook_id = next(( + (user['username'], webhook['id']) + for user in admins + for webhook in webhooks + if webhook['type'] == 'project' + and webhook['organization'] is not None + and webhook['owner']['id'] != user['id'] + and projects[webhook['project']]['owner']['id'] != user['id'] + and not is_org_member(user['id'], webhook['organization']) + )) + + response = delete_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NO_CONTENT + + response = get_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NOT_FOUND + + def test_admin_can_delete_org_webhook(self, find_users, webhooks, is_org_member): + admins = find_users(privilege='admin') + username, webhook_id = next(( + (user['username'], webhook['id']) + for user in admins + for webhook in webhooks + if webhook['type'] == 'organization' + and webhook['organization'] is not None + and webhook['owner']['id'] != user['id'] + and not is_org_member(user['id'], webhook['organization']) + )) + + response = delete_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NO_CONTENT + + response = get_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NOT_FOUND + + @pytest.mark.parametrize('privilege', ['user', 'business']) + def test_project_owner_can_delete_project_webhook(self, privilege, find_users, + webhooks, projects): + users = find_users(privilege=privilege) + username, webhook_id = next(( + (user['username'], webhook['id']) + for user in users + for webhook in webhooks + if webhook['type'] == 'project' + and webhook['organization'] is None + and projects[webhook['project']]['owner']['id'] == user['id'] + )) + + response = delete_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NO_CONTENT + + response = get_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NOT_FOUND + + @pytest.mark.parametrize('privilege', ['user', 'business']) + def test_webhook_owner_can_delete_project_webhook(self, privilege, find_users, + webhooks, projects): + users = find_users(privilege=privilege) + username, webhook_id = next(( + (user['username'], webhook['id']) + for user in users + for webhook in webhooks + if webhook['type'] == 'project' + and webhook['organization'] is None + and webhook['owner']['id'] == user['id'] + )) + + response = delete_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NO_CONTENT + + response = get_method(username, f'webhooks/{webhook_id}') + assert response.status_code == HTTPStatus.NOT_FOUND + + @pytest.mark.parametrize('role, allow', [ + ('owner', True), ('maintainer', True), + ('worker', False), ('supervisor', False) + ]) + def test_member_can_delete_org_webhook(self, role, allow, find_users, organizations, + webhooks): + org_webhooks = [w for w in webhooks if w['type'] == 'organization'] + print(org_webhooks) + username, org_id, webhook_id = next(( + (user['username'], org['id'], webhook['id']) + for org in organizations + for webhook in org_webhooks + for user in find_users(role=role, org=org['id']) + if webhook['organization'] == org['id'] + )) + + if not allow: + response = delete_method(username, f'webhooks/{webhook_id}', org_id=org_id) + assert response.status_code == HTTPStatus.FORBIDDEN + else: + response = delete_method(username, f'webhooks/{webhook_id}', org_id=org_id) + assert response.status_code == HTTPStatus.NO_CONTENT + + response = get_method(username, f'webhooks/{webhook_id}', org_id=org_id) + assert response.status_code == HTTPStatus.NOT_FOUND + + @pytest.mark.parametrize('role, allow', [ + ('owner', True), ('maintainer', True), + ('worker', False), ('supervisor', False) + ]) + def test_member_can_delete_project_webhook_in_org(self, role, allow, find_users, + organizations, projects, webhooks): + proj_webhooks = [w for w in webhooks if w['type'] == 'project'] + username, org_id, webhook_id = next(( + (user['username'], webhook['organization'], webhook['id']) + for org in organizations + for user in find_users(role=role, org=org['id']) + for webhook in proj_webhooks + if webhook['organization'] + and webhook['organization'] == org['id'] + and projects[webhook['project']]['owner']['id'] != user['id'] + and webhook['owner']['id'] != user['id'] + )) + + if not allow: + response = delete_method(username, f'webhooks/{webhook_id}', org_id=org_id) + assert response.status_code == HTTPStatus.FORBIDDEN + else: + response = delete_method(username, f'webhooks/{webhook_id}', org_id=org_id) + assert response.status_code == HTTPStatus.NO_CONTENT + + response = get_method(username, f'webhooks/{webhook_id}', org_id=org_id) + assert response.status_code == HTTPStatus.NOT_FOUND + + @pytest.mark.parametrize('role', ['supervisor']) + def test_member_webhook_staff_can_delete_project_webhook_in_org(self, role, + find_users, organizations, projects, webhooks): + proj_webhooks = [w for w in webhooks if w['type'] == 'project'] + username, org_id, webhook_id = next(( + (user['username'], webhook['organization'], webhook['id']) + for org in organizations + for user in find_users(role=role, org=org['id']) + for webhook in proj_webhooks + if webhook['organization'] + and webhook['organization'] == org['id'] + and (projects[webhook['project']]['owner']['id'] == user['id'] + or webhook['owner']['id'] == user['id']) + )) + + response = delete_method(username, f'webhooks/{webhook_id}', org_id=org_id) + assert response.status_code == HTTPStatus.NO_CONTENT + + response = get_method(username, f'webhooks/{webhook_id}', org_id=org_id) + assert response.status_code == HTTPStatus.NOT_FOUND diff --git a/tests/python/rest_api/test_webhooks_sender.py b/tests/python/rest_api/test_webhooks_sender.py new file mode 100644 index 000000000000..5236d2ac441c --- /dev/null +++ b/tests/python/rest_api/test_webhooks_sender.py @@ -0,0 +1,98 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import json +import os.path as osp +from http import HTTPStatus + +import pytest +from deepdiff import DeepDiff + +from shared.fixtures.init import CVAT_ROOT_DIR, _run +from shared.utils.config import get_method, patch_method, post_method + +# Testing webhook functionality: +# - webhook_receiver container receive post request and return responses with the same body +# - cvat save response body for each delivery +# +# So idea of this testing system is quite simple: +# 1) trigger some webhook +# 2) check that webhook is sent by checking value of `response` field for the last delivery of this webhook + + +def target_url(): + env_data = {} + with open(osp.join(CVAT_ROOT_DIR, "tests", "python", "webhook_receiver", ".env"), "r") as f: + for line in f: + name, value = tuple(line.strip().split("=")) + env_data[name] = value + + container_id = _run( + "docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' test_webhook_receiver_1" + )[0].strip()[1:-1] + + return f'http://{container_id}:{env_data["SERVER_PORT"]}/{env_data["PAYLOAD_ENDPOINT"]}' + + +def webhook_spec(events, project_id=None, webhook_type="organization"): + # Django URL field doesn't allow to use http://webhooks:2020/payload (using alias) + # So we forced to use ip address of webhook receiver container + return { + "target_url": target_url(), + "content_type": "application/json", + "enable_ssl": False, + "events": events, + "is_active": True, + "project_id": project_id, + "type": webhook_type, + } + + +@pytest.mark.usefixtures("changedb") +class TestWebhookProjectEvents: + def test_webhook_project_update(self): + events = ["update:project"] + patch_data = {"name": "new_project_name"} + + # create project + response = post_method("admin1", "projects", {"name": "project"}) + assert response.status_code == HTTPStatus.CREATED + project = response.json() + + # create webhook + response = post_method( + "admin1", "webhooks", webhook_spec(events, project["id"], webhook_type="project") + ) + assert response.status_code == HTTPStatus.CREATED + webhook = response.json() + + # update project + response = patch_method("admin1", f"projects/{project['id']}", patch_data) + assert response.status_code == HTTPStatus.OK + + # get list of deliveries of webhook + response = get_method("admin1", f"webhooks/{webhook['id']}/deliveries") + assert response.status_code == HTTPStatus.OK + + response_data = response.json() + + # check that we sent only one webhook + assert response_data["count"] == 1 + + # check value of payload that CVAT sent + payload = json.loads(response_data["results"][0]["response"]) + assert payload["event"] == events[0] + assert payload["sender"]["username"] == "admin1" + assert payload["before_update"]["name"] == project["name"] + + project.update(patch_data) + assert ( + DeepDiff( + payload["project"], + project, + ignore_order=True, + exclude_paths=["root['updated_date']"], + ) + == {} + ) diff --git a/tests/python/shared/assets/cvat_db/data.json b/tests/python/shared/assets/cvat_db/data.json index 49ca0424a71b..eba496a44c2b 100644 --- a/tests/python/shared/assets/cvat_db/data.json +++ b/tests/python/shared/assets/cvat_db/data.json @@ -36,7 +36,7 @@ "pk": 1, "fields": { "password": "pbkdf2_sha256$260000$DevmxlmLwciP1P6sZs2Qag$U9DFtjTWx96Sk95qY6UXVcvpdQEP2LcoFBftk5D2RKY=", - "last_login": "2022-09-22T14:21:28.429Z", + "last_login": "2022-09-28T12:20:48.633Z", "is_superuser": true, "username": "admin1", "first_name": "Admin", @@ -58,7 +58,7 @@ "pk": 2, "fields": { "password": "pbkdf2_sha256$260000$Pf2xYWXBedoAJ504jyDD8e$8sJ244Ai0xhZrUTelapPNHlEg7CV0cCUaxbcxZtfaug=", - "last_login": "2022-03-17T07:22:09.327Z", + "last_login": "2022-09-28T12:15:35.182Z", "is_superuser": false, "username": "user1", "first_name": "User", @@ -80,7 +80,7 @@ "pk": 3, "fields": { "password": "pbkdf2_sha256$260000$9YZSJ0xF4Kvjsm2Fwflciy$zRpcqAMLaJBbqTRS09NkZovOHtcdy6haZxu++AeoWFo=", - "last_login": "2022-03-28T13:05:05.561Z", + "last_login": "2022-09-28T12:19:33.698Z", "is_superuser": false, "username": "user2", "first_name": "User", @@ -146,7 +146,7 @@ "pk": 6, "fields": { "password": "pbkdf2_sha256$260000$15iUjDNh5gPg5683u1HhOG$fF8hW6AR90o9SCsO/MomzdQFkgQsMUW3YQUlwwiC1vA=", - "last_login": "2021-12-14T19:11:21.048Z", + "last_login": "2022-09-06T07:57:19.879Z", "is_superuser": false, "username": "worker1", "first_name": "Worker", @@ -234,7 +234,7 @@ "pk": 10, "fields": { "password": "pbkdf2_sha256$260000$X4F89IRqnBtojZuHidrwQG$j1+EpXfyvMesHdod4N+dNUfF4WKS2NWFfeGDec/43as=", - "last_login": "2022-03-05T10:31:48.850Z", + "last_login": "2022-09-28T12:17:51.373Z", "is_superuser": false, "username": "business1", "first_name": "Business", @@ -463,6 +463,14 @@ "expire_date": "2022-03-07T10:37:08.963Z" } }, +{ + "model": "sessions.session", + "pk": "9432vwcpkukpdrme8vipuk9rmt4jv6c8", + "fields": { + "session_data": ".eJxVjDsOwyAQBe9CHSFgxS9l-pwBAbsEJxFIxq6s3D225CJp38y8jYW4LjWsg-YwIbsyyS6_W4r5Re0A-Izt0XnubZmnxA-Fn3Twe0d6307376DGUfdagfWIgEaBQVUKkSAsoIs2lFPOSiRJdrcgOSlcsd6CspS1dsrLpNnnC_apN98:1oVTwm:l7XK3LDGUWfyscT3hQeh8nOj0iu1-oh4NcdaMIoEEYg", + "expire_date": "2022-09-20T08:28:40.843Z" + } +}, { "model": "sessions.session", "pk": "9rh2r15lb3xra3kdqjtll5n4zw7ebw95", @@ -543,6 +551,14 @@ "expire_date": "2022-08-02T14:27:42.020Z" } }, +{ + "model": "sessions.session", + "pk": "o8j1a2nv54lfpfrrg44h5h93jj7xxh60", + "fields": { + "session_data": ".eJxVjMsOwiAQRf-FtSG8EZfu_QYyzIBUDSSlXRn_3TbpQrf3nHPfLMK61LiOPMeJ2IVJdvrdEuAztx3QA9q9c-xtmafEd4UfdPBbp_y6Hu7fQYVRt1pk8rKQdLZQAJc8WqdRepJnkMU5j8qJ4AMFoxUqXaxRIDaEypqiEvt8AeKHN58:1odW3U:8KSfPTBjmZAKj1CGyu6KNB7dVP-lx1qvWe3yAiI0lio", + "expire_date": "2022-10-12T12:20:48.638Z" + } +}, { "model": "sessions.session", "pk": "oy4oy702g9qr34fjne8jnxoxvqaiaq26", @@ -577,30 +593,28 @@ }, { "model": "sessions.session", - "pk": "wf6d6vzf4u74l08o0qgbqehei21hibea", + "pk": "vph7z81qem6c9705bnj7xxvzmzod24ck", "fields": { - "session_data": ".eJxVjDEOwjAMRe-SGUUkpHZgZO8ZIttxSAG1UtNOiLtDpQ6w_vfef5lE61LT2nROQzYX48zhd2OSh44byHcab5OVaVzmge2m2J02209Zn9fd_Tuo1Oq3DrGwD040Ro_-nJmJgkgsqAAIioCi0KGKMhU4Mgip6wjRF6JyMu8PBAI5Mw:1nIXJc:oovNJRods5cbviWOWush4H3jDdP8XklEignva_EnQ8Q", - "expire_date": "2022-02-25T14:54:28.092Z" + "session_data": ".eJxVjEEOgjAQRe_StWlmahlal-45A5npFEFNSSisjHdXEha6_e-9_zI9b-vYbzUv_aTmYhDM6XcUTo9cdqJ3LrfZprmsyyR2V-xBq-1mzc_r4f4djFzHbx2G5oxKA4foXCRS9QlEghKDpwBNExWyEw-Z2owOJSTH2IK24smjeX8A-A43ag:1oVRr1:YjNerWaOn53u4nuATnD-oXeI5uqdL6lAyKX4jCG0b94", + "expire_date": "2022-09-20T06:14:35.567Z" } }, { - "model": "authtoken.token", - "pk": "2d5bca87ec38cba82ad1da525431ec3a224385b6", + "model": "sessions.session", + "pk": "wf6d6vzf4u74l08o0qgbqehei21hibea", "fields": { - "user": [ - "admin1" - ], - "created": "2022-06-08T08:32:30.149Z" + "session_data": ".eJxVjDEOwjAMRe-SGUUkpHZgZO8ZIttxSAG1UtNOiLtDpQ6w_vfef5lE61LT2nROQzYX48zhd2OSh44byHcab5OVaVzmge2m2J02209Zn9fd_Tuo1Oq3DrGwD040Ro_-nJmJgkgsqAAIioCi0KGKMhU4Mgip6wjRF6JyMu8PBAI5Mw:1nIXJc:oovNJRods5cbviWOWush4H3jDdP8XklEignva_EnQ8Q", + "expire_date": "2022-02-25T14:54:28.092Z" } }, { "model": "authtoken.token", - "pk": "3952f1aea900fc3daa269473a71c41fac08858b5", + "pk": "4f057576712c65d30847e77456aea605a9df5965", "fields": { "user": [ - "business1" + "admin1" ], - "created": "2022-03-05T10:31:48.838Z" + "created": "2022-09-28T12:20:48.631Z" } }, { @@ -811,6 +825,19 @@ "role": "worker" } }, +{ + "model": "organizations.membership", + "pk": 13, + "fields": { + "user": [ + "user5" + ], + "organization": 1, + "is_active": true, + "joined_date": "2022-09-28T13:11:37.839Z", + "role": "supervisor" + } +}, { "model": "organizations.invitation", "pk": "5FjIXya6fTGvlRpauFvi2QN1wDOqo1V9REB5rJinDR8FZO9gr0qmtWpghsCte8Y1", @@ -910,6 +937,17 @@ "membership": 9 } }, +{ + "model": "organizations.invitation", + "pk": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9", + "fields": { + "created_date": "2022-09-28T13:11:37.839Z", + "owner": [ + "admin1" + ], + "membership": 13 + } +}, { "model": "organizations.invitation", "pk": "mFpVV2Yh39uUdU8IpigSxvuPegqi8sjxFi6P9Jdy6fBE8Ky9Juzi1KjeGDQsizSS", @@ -2196,7 +2234,7 @@ "assignee": null, "bug_tracker": "", "created_date": "2022-06-08T08:32:45.521Z", - "updated_date": "2022-06-08T08:33:20.759Z", + "updated_date": "2022-09-28T12:26:54.279Z", "status": "annotation", "organization": 2, "source_storage": null, @@ -2214,13 +2252,53 @@ "assignee": null, "bug_tracker": "", "created_date": "2022-09-22T14:21:53.791Z", - "updated_date": "2022-09-23T11:57:02.088Z", + "updated_date": "2022-09-28T12:26:49.493Z", "status": "annotation", "organization": 2, "source_storage": 5, "target_storage": 6 } }, +{ + "model": "engine.project", + "pk": 6, + "fields": { + "name": "user1_project", + "owner": [ + "user1" + ], + "assignee": [ + "business4" + ], + "bug_tracker": "", + "created_date": "2022-09-28T12:15:50.768Z", + "updated_date": "2022-09-28T12:25:54.563Z", + "status": "annotation", + "organization": null, + "source_storage": 9, + "target_storage": 10 + } +}, +{ + "model": "engine.project", + "pk": 7, + "fields": { + "name": "admin1_project", + "owner": [ + "admin1" + ], + "assignee": [ + "worker4" + ], + "bug_tracker": "", + "created_date": "2022-09-28T12:26:25.296Z", + "updated_date": "2022-09-28T12:26:29.285Z", + "status": "annotation", + "organization": null, + "source_storage": 11, + "target_storage": 12 + } +}, { "model": "engine.task", "pk": 2, @@ -3794,6 +3872,30 @@ "parent": 22 } }, +{ + "model": "engine.label", + "pk": 27, + "fields": { + "task": null, + "project": 6, + "name": "label_0", + "color": "#bde94a", + "type": "any", + "parent": null + } +}, +{ + "model": "engine.label", + "pk": 28, + "fields": { + "task": null, + "project": 7, + "name": "label_0", + "color": "#bde94a", + "type": "any", + "parent": null + } +}, { "model": "engine.skeleton", "pk": 1, @@ -5647,6 +5749,167 @@ "cloud_storage_id": null } }, +{ + "model": "engine.storage", + "pk": 9, + "fields": { + "location": "local", + "cloud_storage_id": null + } +}, +{ + "model": "engine.storage", + "pk": 10, + "fields": { + "location": "local", + "cloud_storage_id": null + } +}, +{ + "model": "engine.storage", + "pk": 11, + "fields": { + "location": "local", + "cloud_storage_id": null + } +}, +{ + "model": "engine.storage", + "pk": 12, + "fields": { + "location": "local", + "cloud_storage_id": null + } +}, +{ + "model": "webhooks.webhook", + "pk": 1, + "fields": { + "target_url": "http://example.com/", + "description": "", + "events": "delete:task,update:task,create:task,update:job", + "type": "project", + "content_type": "application/json", + "secret": "", + "is_active": true, + "enable_ssl": true, + "created_date": "2022-09-28T12:16:28.311Z", + "updated_date": "2022-09-28T12:16:28.311Z", + "owner": [ + "user1" + ], + "project": 6, + "organization": null + } +}, +{ + "model": "webhooks.webhook", + "pk": 2, + "fields": { + "target_url": "http://example.com/", + "description": "", + "events": "delete:comment,update:issue,update:comment,update:job,update:project,delete:task,create:comment,delete:issue,update:task,create:task,create:issue", + "type": "project", + "content_type": "application/json", + "secret": "", + "is_active": true, + "enable_ssl": true, + "created_date": "2022-09-28T12:18:12.412Z", + "updated_date": "2022-09-28T12:18:12.412Z", + "owner": [ + "business1" + ], + "project": 1, + "organization": null + } +}, +{ + "model": "webhooks.webhook", + "pk": 3, + "fields": { + "target_url": "http://example.com", + "description": "", + "events": "update:issue,delete:issue,create:issue,update:project", + "type": "project", + "content_type": "application/json", + "secret": "", + "is_active": true, + "enable_ssl": true, + "created_date": "2022-09-28T12:19:49.744Z", + "updated_date": "2022-09-28T12:19:49.744Z", + "owner": [ + "user2" + ], + "project": 3, + "organization": 2 + } +}, +{ + "model": "webhooks.webhook", + "pk": 5, + "fields": { + "target_url": "http://example.com", + "description": "", + "events": "delete:invitation,delete:project,create:project,delete:comment,update:organization,update:issue,update:task,update:comment,update:job,update:project,delete:task,create:comment,delete:issue,delete:membership,create:invitation,create:task,create:issue,update:membership", + "type": "organization", + "content_type": "application/json", + "secret": "", + "is_active": true, + "enable_ssl": true, + "created_date": "2022-09-28T12:51:06.703Z", + "updated_date": "2022-09-28T12:51:06.703Z", + "owner": [ + "admin1" + ], + "project": null, + "organization": 1 + } +}, +{ + "model": "webhooks.webhookdelivery", + "pk": 8, + "fields": { + "webhook": 5, + "event": "create:invitation", + "status_code": 200, + "redelivery": false, + "created_date": "2022-09-28T13:11:37.850Z", + "updated_date": "2022-09-28T13:11:38.311Z", + "changed_fields": "", + "request": { + "event": "create:invitation", + "sender": { + "id": 1, + "url": "http://localhost:8080/api/users/1", + "username": "admin1", + "last_name": "First", + "first_name": "Admin" + }, + "invitation": { + "key": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9", + "role": "supervisor", + "user": { + "id": 19, + "url": "http://localhost:8080/api/users/19", + "username": "user5", + "last_name": "Fifth", + "first_name": "User" + }, + "owner": { + "id": 1, + "url": "http://localhost:8080/api/users/1", + "username": "admin1", + "last_name": "First", + "first_name": "Admin" + }, + "created_date": "2022-09-28T13:11:37.839853Z", + "organization": 1 + }, + "webhook_id": 5 + }, + "response": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n" + } +}, { "model": "admin.logentry", "pk": 1, diff --git a/tests/python/shared/assets/invitations.json b/tests/python/shared/assets/invitations.json index 6c225883d5a3..bcdbb03ce888 100644 --- a/tests/python/shared/assets/invitations.json +++ b/tests/python/shared/assets/invitations.json @@ -1,8 +1,28 @@ { - "count": 10, + "count": 11, "next": null, "previous": null, "results": [ + { + "created_date": "2022-09-28T13:11:37.839000Z", + "key": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9", + "organization": 1, + "owner": { + "first_name": "Admin", + "id": 1, + "last_name": "First", + "url": "http://localhost:8080/api/users/1", + "username": "admin1" + }, + "role": "supervisor", + "user": { + "first_name": "User", + "id": 19, + "last_name": "Fifth", + "url": "http://localhost:8080/api/users/19", + "username": "user5" + } + }, { "created_date": "2022-02-24T21:29:21.978000Z", "key": "Fi3WRUhFxTWpMiVpdwNR2CGyhgcIXSCUYgPCugPq72QUOgHz9NSMOGiKS3PfJ7Ql", diff --git a/tests/python/shared/assets/memberships.json b/tests/python/shared/assets/memberships.json index 3303832a7613..2c37c8833937 100644 --- a/tests/python/shared/assets/memberships.json +++ b/tests/python/shared/assets/memberships.json @@ -1,8 +1,23 @@ { - "count": 12, + "count": 13, "next": null, "previous": null, "results": [ + { + "id": 13, + "invitation": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9", + "is_active": true, + "joined_date": "2022-09-28T13:11:37.839000Z", + "organization": 1, + "role": "supervisor", + "user": { + "first_name": "User", + "id": 19, + "last_name": "Fifth", + "url": "http://localhost:8080/api/users/19", + "username": "user5" + } + }, { "id": 12, "invitation": "Fi3WRUhFxTWpMiVpdwNR2CGyhgcIXSCUYgPCugPq72QUOgHz9NSMOGiKS3PfJ7Ql", diff --git a/tests/python/shared/assets/projects.json b/tests/python/shared/assets/projects.json index f464aba80589..de8a218ac542 100644 --- a/tests/python/shared/assets/projects.json +++ b/tests/python/shared/assets/projects.json @@ -1,8 +1,104 @@ { - "count": 5, + "count": 7, "next": null, "previous": null, "results": [ + { + "assignee": { + "first_name": "Worker", + "id": 9, + "last_name": "Fourth", + "url": "http://localhost:8080/api/users/9", + "username": "worker4" + }, + "bug_tracker": "", + "created_date": "2022-09-28T12:26:25.296000Z", + "dimension": null, + "id": 7, + "labels": [ + { + "attributes": [], + "color": "#bde94a", + "has_parent": false, + "id": 28, + "name": "label_0", + "sublabels": [], + "type": "any" + } + ], + "name": "admin1_project", + "organization": null, + "owner": { + "first_name": "Admin", + "id": 1, + "last_name": "First", + "url": "http://localhost:8080/api/users/1", + "username": "admin1" + }, + "source_storage": { + "cloud_storage_id": null, + "id": 11, + "location": "local" + }, + "status": "annotation", + "target_storage": { + "cloud_storage_id": null, + "id": 12, + "location": "local" + }, + "task_subsets": [], + "tasks": [], + "updated_date": "2022-09-28T12:26:29.285000Z", + "url": "http://localhost:8080/api/projects/7" + }, + { + "assignee": { + "first_name": "Business", + "id": 13, + "last_name": "Fourth", + "url": "http://localhost:8080/api/users/13", + "username": "business4" + }, + "bug_tracker": "", + "created_date": "2022-09-28T12:15:50.768000Z", + "dimension": null, + "id": 6, + "labels": [ + { + "attributes": [], + "color": "#bde94a", + "has_parent": false, + "id": 27, + "name": "label_0", + "sublabels": [], + "type": "any" + } + ], + "name": "user1_project", + "organization": null, + "owner": { + "first_name": "User", + "id": 2, + "last_name": "First", + "url": "http://localhost:8080/api/users/2", + "username": "user1" + }, + "source_storage": { + "cloud_storage_id": null, + "id": 9, + "location": "local" + }, + "status": "annotation", + "target_storage": { + "cloud_storage_id": null, + "id": 10, + "location": "local" + }, + "task_subsets": [], + "tasks": [], + "updated_date": "2022-09-28T12:25:54.563000Z", + "url": "http://localhost:8080/api/projects/6" + }, { "assignee": null, "bug_tracker": "", @@ -212,7 +308,7 @@ "tasks": [ 14 ], - "updated_date": "2022-09-23T11:57:02.088000Z", + "updated_date": "2022-09-28T12:26:49.493000Z", "url": "http://localhost:8080/api/projects/5" }, { @@ -257,7 +353,7 @@ "tasks": [ 13 ], - "updated_date": "2022-06-08T08:33:20.759000Z", + "updated_date": "2022-09-28T12:26:54.279000Z", "url": "http://localhost:8080/api/projects/4" }, { diff --git a/tests/python/shared/assets/users.json b/tests/python/shared/assets/users.json index 2eded1dde47f..8560b7a34d29 100644 --- a/tests/python/shared/assets/users.json +++ b/tests/python/shared/assets/users.json @@ -166,7 +166,7 @@ "is_active": true, "is_staff": false, "is_superuser": false, - "last_login": "2022-03-05T10:31:48.850000Z", + "last_login": "2022-09-28T12:17:51.373000Z", "last_name": "First", "url": "http://localhost:8080/api/users/10", "username": "business1" @@ -230,7 +230,7 @@ "is_active": true, "is_staff": false, "is_superuser": false, - "last_login": "2021-12-14T19:11:21.048000Z", + "last_login": "2022-09-06T07:57:19.879000Z", "last_name": "First", "url": "http://localhost:8080/api/users/6", "username": "worker1" @@ -278,7 +278,7 @@ "is_active": true, "is_staff": false, "is_superuser": false, - "last_login": "2022-03-28T13:05:05.561000Z", + "last_login": "2022-09-28T12:19:33.698000Z", "last_name": "Second", "url": "http://localhost:8080/api/users/3", "username": "user2" @@ -294,7 +294,7 @@ "is_active": true, "is_staff": false, "is_superuser": false, - "last_login": "2022-03-17T07:22:09.327000Z", + "last_login": "2022-09-28T12:15:35.182000Z", "last_name": "First", "url": "http://localhost:8080/api/users/2", "username": "user1" @@ -310,7 +310,7 @@ "is_active": true, "is_staff": true, "is_superuser": true, - "last_login": "2022-09-22T14:21:28.429000Z", + "last_login": "2022-09-28T12:20:48.633000Z", "last_name": "First", "url": "http://localhost:8080/api/users/1", "username": "admin1" diff --git a/tests/python/shared/assets/webhooks.json b/tests/python/shared/assets/webhooks.json new file mode 100644 index 000000000000..ba8f754adc96 --- /dev/null +++ b/tests/python/shared/assets/webhooks.json @@ -0,0 +1,138 @@ +{ + "count": 4, + "next": null, + "previous": null, + "results": [ + { + "content_type": "application/json", + "created_date": "2022-09-28T12:51:06.703000Z", + "description": "", + "enable_ssl": true, + "events": [ + "create:comment", + "create:invitation", + "create:issue", + "create:project", + "create:task", + "delete:comment", + "delete:invitation", + "delete:issue", + "delete:membership", + "delete:project", + "delete:task", + "update:comment", + "update:issue", + "update:job", + "update:membership", + "update:organization", + "update:project", + "update:task" + ], + "id": 5, + "is_active": true, + "last_delivery_date": "2022-09-28T13:11:38.311000Z", + "last_status": 200, + "organization": 1, + "owner": { + "first_name": "Admin", + "id": 1, + "last_name": "First", + "url": "http://localhost:8080/api/users/1", + "username": "admin1" + }, + "project": null, + "target_url": "http://example.com", + "type": "organization", + "updated_date": "2022-09-28T12:51:06.703000Z", + "url": "http://localhost:8080/api/webhooks/5" + }, + { + "content_type": "application/json", + "created_date": "2022-09-28T12:19:49.744000Z", + "description": "", + "enable_ssl": true, + "events": [ + "create:issue", + "delete:issue", + "update:issue", + "update:project" + ], + "id": 3, + "is_active": true, + "organization": 2, + "owner": { + "first_name": "User", + "id": 3, + "last_name": "Second", + "url": "http://localhost:8080/api/users/3", + "username": "user2" + }, + "project": 3, + "target_url": "http://example.com", + "type": "project", + "updated_date": "2022-09-28T12:19:49.744000Z", + "url": "http://localhost:8080/api/webhooks/3" + }, + { + "content_type": "application/json", + "created_date": "2022-09-28T12:18:12.412000Z", + "description": "", + "enable_ssl": true, + "events": [ + "create:comment", + "create:issue", + "create:task", + "delete:comment", + "delete:issue", + "delete:task", + "update:comment", + "update:issue", + "update:job", + "update:project", + "update:task" + ], + "id": 2, + "is_active": true, + "organization": null, + "owner": { + "first_name": "Business", + "id": 10, + "last_name": "First", + "url": "http://localhost:8080/api/users/10", + "username": "business1" + }, + "project": 1, + "target_url": "http://example.com/", + "type": "project", + "updated_date": "2022-09-28T12:18:12.412000Z", + "url": "http://localhost:8080/api/webhooks/2" + }, + { + "content_type": "application/json", + "created_date": "2022-09-28T12:16:28.311000Z", + "description": "", + "enable_ssl": true, + "events": [ + "create:task", + "delete:task", + "update:job", + "update:task" + ], + "id": 1, + "is_active": true, + "organization": null, + "owner": { + "first_name": "User", + "id": 2, + "last_name": "First", + "url": "http://localhost:8080/api/users/2", + "username": "user1" + }, + "project": 6, + "target_url": "http://example.com/", + "type": "project", + "updated_date": "2022-09-28T12:16:28.311000Z", + "url": "http://localhost:8080/api/webhooks/1" + } + ] +} \ No newline at end of file diff --git a/tests/python/shared/fixtures/data.py b/tests/python/shared/fixtures/data.py index b40dc830afc6..e585af977f83 100644 --- a/tests/python/shared/fixtures/data.py +++ b/tests/python/shared/fixtures/data.py @@ -83,6 +83,11 @@ def issues(): with open(osp.join(ASSETS_DIR, 'issues.json')) as f: return Container(json.load(f)['results']) +@pytest.fixture(scope='session') +def webhooks(): + with open(osp.join(ASSETS_DIR, 'webhooks.json')) as f: + return Container(json.load(f)['results']) + @pytest.fixture(scope='session') def users_by_name(users): return {user['username']: user for user in users} diff --git a/tests/python/shared/fixtures/init.py b/tests/python/shared/fixtures/init.py index 309e3158630a..d74a6bb068ad 100644 --- a/tests/python/shared/fixtures/init.py +++ b/tests/python/shared/fixtures/init.py @@ -27,7 +27,11 @@ DC_FILES = [ osp.join(CVAT_ROOT_DIR, dc_file) - for dc_file in ("docker-compose.dev.yml", "tests/docker-compose.minio.yml") + for dc_file in ( + "docker-compose.dev.yml", + "tests/docker-compose.minio.yml", + "tests/docker-compose.webhook.yml" + ) ] + CONTAINER_NAME_FILES @@ -208,7 +212,9 @@ def start_services(rebuild=False): ) _run( - f"docker-compose -p {PREFIX} -f {' -f '.join(DC_FILES)} up -d " + f"docker-compose -p {PREFIX} " + + "--env-file " + osp.join(CVAT_ROOT_DIR, "tests", "python", "webhook_receiver", ".env") + + f" -f {' -f '.join(DC_FILES)} up -d " + "--build" * rebuild, capture_output=False, ) @@ -251,7 +257,9 @@ def services(request): if stop: _run( - f"docker-compose -p {PREFIX} -f {' -f '.join(DC_FILES)} down -v", + f"docker-compose -p {PREFIX} " + + "--env-file " + osp.join(CVAT_ROOT_DIR, "tests", "python", "webhook_receiver", ".env") + + f" -f {' -f '.join(DC_FILES)} down -v", capture_output=False, ) pytest.exit("All testing containers are stopped", returncode=0) diff --git a/tests/python/shared/utils/dump_objects.py b/tests/python/shared/utils/dump_objects.py index 886f15b3a73e..aa7b99e4133a 100644 --- a/tests/python/shared/utils/dump_objects.py +++ b/tests/python/shared/utils/dump_objects.py @@ -9,7 +9,7 @@ if __name__ == '__main__': annotations = {} for obj in ['user', 'project', 'task', 'job', 'organization', 'membership', - 'invitation', 'cloudstorage', 'issue']: + 'invitation', 'cloudstorage', 'issue', 'webhook']: response = get_method('admin1', f'{obj}s', page_size='all') with open(osp.join(ASSETS_DIR, f'{obj}s.json'), 'w') as f: json.dump(response.json(), f, indent=2, sort_keys=True) diff --git a/tests/python/webhook_receiver/.env b/tests/python/webhook_receiver/.env new file mode 100644 index 000000000000..c5c45e87290f --- /dev/null +++ b/tests/python/webhook_receiver/.env @@ -0,0 +1,2 @@ +SERVER_PORT=2020 +PAYLOAD_ENDPOINT=payload \ No newline at end of file diff --git a/tests/python/webhook_receiver/server.py b/tests/python/webhook_receiver/server.py new file mode 100644 index 000000000000..fe0751963ccf --- /dev/null +++ b/tests/python/webhook_receiver/server.py @@ -0,0 +1,33 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import re +import os +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, HTTPServer + + +class RequestHandler(BaseHTTPRequestHandler): + def do_POST(self): + TARGET_URL_PATTERN = re.compile(r"/" + os.getenv("PAYLOAD_ENDPOINT")) + if not re.search(TARGET_URL_PATTERN, self.path): + return + + self.send_response(HTTPStatus.OK) + self.end_headers() + + request_body = self.rfile.read(int(self.headers["content-length"])) + self.wfile.write(request_body) + + +def main(): + TARGET_HOST = "0.0.0.0" + TARGET_PORT = int(os.getenv("SERVER_PORT")) + + webhook_receiver = HTTPServer((TARGET_HOST, TARGET_PORT), RequestHandler) + webhook_receiver.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/yarn.lock b/yarn.lock index 623da490b613..c26c2c313ec9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4644,11 +4644,6 @@ eslint-plugin-cypress@^2.11.2: dependencies: globals "^11.12.0" -eslint-plugin-header@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz#6ce512432d57675265fac47292b50d1eff11acd6" - integrity sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg== - eslint-plugin-import@^2.22.1: version "2.26.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b"