diff --git a/api-docs/openapi.json b/api-docs/openapi.json index e344f94be..1a122a3ff 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1974,18 +1974,12 @@ }, "post": { "tags": [ - "Organization" + "Registry Organization" ], - "summary": "Retrieves all organizations (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all organizations

", - "operationId": "orgAll", + "summary": "Creates an organization (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates a new organization

", + "operationId": "orgCreateSingle", "parameters": [ - { - "$ref": "#/components/parameters/pageQuery" - }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2057,6 +2051,29 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "../schemas/registry-org/SecretariatOrg.json" + }, + { + "$ref": "../schemas/registry-org/CNAOrg.json" + }, + { + "$ref": "../schemas/registry-org/ADPOrg.json" + }, + { + "$ref": "../schemas/registry-org/BulkDownloadOrg.json" + } + ] + } + } + } } } }, @@ -2597,9 +2614,6 @@ { "$ref": "#/components/parameters/active_roles_remove" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2887,10 +2901,14 @@ "operationId": "orgAll", "parameters": [ { - "$ref": "#/components/parameters/pageQuery" + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } }, { - "$ref": "#/components/parameters/registry" + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -2980,9 +2998,6 @@ "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates an organization

", "operationId": "orgCreateSingle", "parameters": [ - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3067,14 +3082,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/create-org-request.json" - }, - { - "$ref": "../schemas/registry-org/create-registry-org-request.json" - } - ] + "$ref": "../schemas/org/create-org-request.json" } } } @@ -3099,9 +3107,6 @@ }, "description": "The shortname or UUID of the organization" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3118,14 +3123,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/get-org-response.json" - }, - { - "$ref": "../schemas/registry-org/get-registry-org-response.json" - } - ] + "$ref": "../schemas/org/get-org-response.json" } } } @@ -3201,6 +3199,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/id_quota" }, @@ -3216,9 +3221,6 @@ { "$ref": "#/components/parameters/active_roles_remove" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3235,14 +3237,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/update-org-response.json" - }, - { - "$ref": "../schemas/registry-org/update-registry-org-response.json" - } - ] + "$ref": "../schemas/org/update-org-response.json" } } } @@ -3318,9 +3313,6 @@ }, "description": "The shortname of the organization" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3337,14 +3329,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/get-org-quota-response.json" - }, - { - "$ref": "../schemas/registry-org/get-registry-org-quota-response.json" - } - ] + "$ref": "../schemas/org/get-org-quota-response.json" } } } @@ -3421,10 +3406,14 @@ "description": "The shortname of the organization" }, { - "$ref": "#/components/parameters/pageQuery" + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } }, { - "$ref": "#/components/parameters/registry" + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3442,14 +3431,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/user/list-users-response.json" - }, - { - "$ref": "../schemas/registry-user/list-registry-users-response.json" - } - ] + "$ref": "../schemas/user/list-users-response.json" } } } @@ -3525,9 +3507,6 @@ }, "description": "The shortname of the organization" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3765,9 +3744,6 @@ { "$ref": "#/components/parameters/orgShortname" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, diff --git a/schemas/registry-org/ADPOrg.json b/schemas/registry-org/ADPOrg.json new file mode 100644 index 000000000..be9829003 --- /dev/null +++ b/schemas/registry-org/ADPOrg.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "ADPOrg", + "type": "object", + "title": "CVE ADP Organization", + "description": "Schema for a CVE ADP Organization", + "allOf": [ + { "$ref": "./BaseOrg.json" }, + { + "properties": { + "authority": { + "const": ["ADP"] + } + } + } + ] +} diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json new file mode 100644 index 000000000..87f1b1e57 --- /dev/null +++ b/schemas/registry-org/BaseOrg.json @@ -0,0 +1,121 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "./BaseOrg.json", + "type": "object", + "title": "CVE Base Organization", + "description": "Base schema for a CVE Organization", + "definitions": { + "uuidType": { + "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", + "type": "string", + "format": "uuid", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "uriType": { + "description": "A universal resource identifier (URI), according to [RFC 3986](https://tools.ietf.org/html/rfc3986).", + "type": "string", + "format": "uri", + "pattern": "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?", + "minLength": 1, + "maxLength": 2048 + }, + "shortName": { + "description": "A 2-32 character name that can be used to complement an organization's UUID.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "longName": { + "description": "A 1-256 character name that can be used to complement an organization's short_name.", + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "authority": { + "description": "The authority (role) of this organization within the CVE program", + "type": "string", + "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] + } + }, + "properties": { + "UUID": { + "$ref": "#/definitions/uuidType" + }, + "short_name": { + "$ref": "#/definitions/shortName" + }, + "long_name": { + "$ref": "#/definitions/longName" + }, + "aliases": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/authority" + } + }, + "root_or_tlr": { + "type": "boolean" + }, + "reports_to": { + "$ref": "#/definitions/uuidType" + }, + "users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "admins": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "contact_info": { + "type": "object", + "properties": { + "additional_contact_users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + }, + "poc_phone": { + "type": "string" + }, + "org_email": { + "type": "string", + "format": "email" + }, + "website": { + "type": "string", + "format": "uri", + "description": "Organization's website URL" + } + }, + "additionalProperties": false + } + }, + "required": [ + "short_name", + "long_name" + ] +} \ No newline at end of file diff --git a/schemas/registry-org/BulkDownloadOrg.json b/schemas/registry-org/BulkDownloadOrg.json new file mode 100644 index 000000000..cabc0777a --- /dev/null +++ b/schemas/registry-org/BulkDownloadOrg.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "BaseOrg", + "type": "object", + "title": "CVE Bulk Download Organization", + "description": "Schema for a CVE Bulk Download Organization", + "allOf": [ + { "$ref": "./BaseOrg.json" }, + { + "properties": { + "authority": { + "const": ["BULK_DOWNLOAD"] + } + } + } + ] +} diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json new file mode 100644 index 000000000..0402e8338 --- /dev/null +++ b/schemas/registry-org/CNAOrg.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "$id": "CNAOrg", + "title": "CVE CNA Organization", + "description": "Schema for a CVE CNA Organization", + "allOf": [ + { "$ref": "./BaseOrg.json" }, + { + "properties": { + "authority": { + "const": ["CNA"] + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "./BaseOrg.json#/definitions/uuidType" + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0 + }, + "soft_quota": { + "type": "integer", + "minimum": 0 + }, + "charter_or_scope": { + "$ref": "/BaseOrg#/definitions/uriType" + }, + "disclosure_policy": { + "$ref": "/BaseOrg#/definitions/uriType" + }, + "product_list": { + "$ref": "/BaseOrg#/definitions/uriType" + } + }, + "required": ["hard_quota"] + } + ] +} diff --git a/schemas/registry-org/SecretariatOrg.json b/schemas/registry-org/SecretariatOrg.json new file mode 100644 index 000000000..469bd7df5 --- /dev/null +++ b/schemas/registry-org/SecretariatOrg.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "SecretariatOrg", + "type": "object", + "title": "CVE Secretariat Organization", + "description": "Schema for a CVE Secretariat Organization", + "allOf": [ + { "$ref": "./BaseOrg.json" }, + { + "properties": { + "authority": { + "const": ["SECRETARIAT"] + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "./BaseOrg.json#/definitions/uuidType" + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0 + }, + "soft_quota": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["hard_quota"] + } + ] +} diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 481a80851..b7fa78bfa 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -20,23 +20,12 @@ }, "description": "Alternative names or aliases for the organization" }, - "cve_program_org_function": { - "type": "string", - "enum": ["CNA", "ADP", "Root", "Secretariat"], - "description": "The organization's function within the CVE program" - }, "authority": { - "type": "object", - "properties": { - "active_roles": { - "type": "array", + "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Root", "Secretariat"] + "enum": ["CNA", "ADP", "BULK_DOWNLOAD", "SECRETARIAT"] } - } - }, - "required": ["active_roles"] }, "reports_to": { "type": ["string", "null"], @@ -117,10 +106,7 @@ }, "required": [ "short_name", - "cve_program_org_function", "authority", - "root_or_tlr", - "users", - "contact_info" + "long_name" ] } diff --git a/src/constants/index.js b/src/constants/index.js index d06ad3e03..310e202bc 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,6 +44,8 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'root_or_tlr', 'charter_or', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'cna_role_type', 'cna_country', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list'], + JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles'], USER_ROLE_ENUM: { ADMIN: 'ADMIN' }, diff --git a/src/controller/audit.controller/audit.controller.js b/src/controller/audit.controller/audit.controller.js index 1540f41b6..771b9a66c 100644 --- a/src/controller/audit.controller/audit.controller.js +++ b/src/controller/audit.controller/audit.controller.js @@ -8,7 +8,7 @@ const validateUUID = require('uuid').validate * Create a new audit document * Called by POST /api/audit/org/ */ -async function createAuditDocument (req, res, next) { +async function createAuditDocumentForOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -72,9 +72,25 @@ async function createAuditDocument (req, res, next) { await session.abortTransaction() return res.status(400).json(error.missingRequiredField('change_author')) } + + // Process entry immediately after validation + returnValue = await repo.appendToAuditHistoryForOrg( + body.target_uuid, + entry.audit_object, + entry.change_author, + { session } + ) } + } else { + // Create audit document with initial empty entry or default entry + returnValue = await repo.appendToAuditHistoryForOrg( + body.target_uuid, + body.audit_object || {}, + body.change_author || req.ctx.org, + { session } + ) } - returnValue = await repo.createAuditDocument(body, { session }) + await session.commitTransaction() logger.info({ @@ -100,7 +116,7 @@ async function createAuditDocument (req, res, next) { * Called by PUT /api/audit/org/ * Allows for multiple appends in a single request */ -async function appendToAuditHistory (req, res, next) { +async function appendToAuditHistoryForOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -149,7 +165,7 @@ async function appendToAuditHistory (req, res, next) { } // Append this history entry - returnValue = await repo.appendToAuditHistory( + returnValue = await repo.appendToAuditHistoryForOrg( body.target_uuid, entry.audit_object, entry.change_author, @@ -190,7 +206,7 @@ async function appendToAuditHistory (req, res, next) { * Get all audit documents * Called by GET /api/audit/org/ */ -async function getAllAuditDocuments (req, res, next) { +async function getAllOrgAuditDocuments (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -213,7 +229,7 @@ async function getAllAuditDocuments (req, res, next) { * Get audit document by its document UUID * Called by GET /api/audit/org/document/:document_uuid */ -async function getAuditByDocumentUUID (req, res, next) { +async function getOrgAuditByDocumentUUID (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -247,48 +263,53 @@ async function getAuditByDocumentUUID (req, res, next) { next(err) } } + /** - * Get audit history by target UUID - * Called by GET /api/audit/org/:target_uuid - * TODO: remove comment-> I changed parameter name from org_identifier to target_uuid to be more generic. + * Get audit history by target identifier (shortname or UUID) + * Called by GET /api/audit/org/:identifier */ -async function getAuditByTargetUUID (req, res, next) { +async function getOrgAuditByOrgIdentifier (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const targetUUID = req.ctx.params.target_uuid + const identifier = req.ctx.params.org_identifier + const identifierIsUUID = validateUUID(identifier) let returnValue - if (!targetUUID) { - logger.info({ uuid: req.ctx.uuid, message: 'Missing target_uuid parameter' }) - return res.status(400).json(error.missingRequiredField('target_uuid')) - } - - if (!validateUUID(targetUUID)) { - logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' }) - return res.status(400).json(error.invalidUUID('target_uuid')) + if (!identifier) { + return res.status(400).json(error.missingRequiredField('identifier')) } try { session.startTransaction() - // Find the target organization - const targetOrg = await orgRepo.findOneByUUID(targetUUID, { session }) + // Find the target organization by either UUID or shortname + const targetOrg = identifierIsUUID + ? await orgRepo.findOneByUUID(identifier, { session }) + : await orgRepo.findOneByShortName(identifier, { session }) + if (!targetOrg) { - logger.info({ uuid: req.ctx.uuid, message: `No organization found with UUID ${targetUUID}` }) + logger.info({ + uuid: req.ctx.uuid, + message: `No organization found with ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier}` + }) await session.abortTransaction() - return res.status(404).json(error.orgDne(targetUUID)) + return res.status(404).json(error.orgDne(identifier)) } - // TODO: confirm middleware is checking admin and secretariat permissions properly + // Get the org's UUID for audit lookup + const targetUUID = targetOrg.UUID returnValue = await repo.findOneByTargetUUID(targetUUID, { session }) if (!returnValue) { - logger.info({ uuid: req.ctx.uuid, message: `No audit history found for target UUID ${targetUUID}` }) + logger.info({ + uuid: req.ctx.uuid, + message: `No audit history found for organization ${identifier} (UUID: ${targetUUID})` + }) await session.abortTransaction() - return res.status(404).json(error.auditDneByTarget(targetUUID)) + return res.status(404).json(error.auditDneByTarget(identifier)) } await session.commitTransaction() @@ -301,7 +322,7 @@ async function getAuditByTargetUUID (req, res, next) { logger.info({ uuid: req.ctx.uuid, - message: `Audit history for target UUID ${targetUUID} sent to user ${req.ctx.user}` + message: `Audit history for ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier} sent to user ${req.ctx.user}` }) return res.status(200).json(returnValue) } catch (err) { @@ -317,18 +338,14 @@ async function getLastXChanges (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() - const targetUUID = req.ctx.params.target_uuid + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const identifier = req.ctx.params.org_identifier + const identifierIsUUID = validateUUID(identifier) const numberOfChanges = parseInt(req.ctx.params.number_of_changes) let returnValue - if (!targetUUID) { - logger.info({ uuid: req.ctx.uuid, message: 'Missing org_identifier parameter' }) - return res.status(400).json(error.missingRequiredField('org_identifier')) - } - - if (!validateUUID(targetUUID)) { - logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' }) - return res.status(400).json(error.invalidUUID('target_uuid')) + if (!identifier) { + return res.status(400).json(error.missingRequiredField('identifier')) } if (isNaN(numberOfChanges) || numberOfChanges < 1) { @@ -339,6 +356,23 @@ async function getLastXChanges (req, res, next) { try { session.startTransaction() + // Find the target organization by either UUID or shortname + const targetOrg = identifierIsUUID + ? await orgRepo.findOneByUUID(identifier, { session }) + : await orgRepo.findOneByShortName(identifier, { session }) + + if (!targetOrg) { + logger.info({ + uuid: req.ctx.uuid, + message: `No organization found with ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier}` + }) + await session.abortTransaction() + return res.status(404).json(error.orgDne(identifier)) + } + + // Get the org's UUID for audit lookup + const targetUUID = targetOrg.UUID + const lastChanges = await repo.getLastXChanges(targetUUID, numberOfChanges, { session }) if (!lastChanges || lastChanges.length === 0) { @@ -362,7 +396,7 @@ async function getLastXChanges (req, res, next) { logger.info({ uuid: req.ctx.uuid, - message: `Last ${numberOfChanges} changes for ${targetUUID} sent to user ${req.ctx.user}` + message: `Last ${numberOfChanges} changes for ${identifier} sent to user ${req.ctx.user}` }) return res.status(200).json(returnValue) } catch (err) { @@ -371,10 +405,10 @@ async function getLastXChanges (req, res, next) { } module.exports = { - AUDIT_CREATE_SINGLE: createAuditDocument, - AUDIT_UPDATE: appendToAuditHistory, - AUDIT_GET_ALL: getAllAuditDocuments, - AUDIT_GET_BY_UUID: getAuditByDocumentUUID, - AUDIT_GET_BY_TARGET_UUID: getAuditByTargetUUID, + AUDIT_CREATE_SINGLE: createAuditDocumentForOrg, + AUDIT_UPDATE: appendToAuditHistoryForOrg, + AUDIT_GET_ALL: getAllOrgAuditDocuments, + AUDIT_GET_BY_UUID: getOrgAuditByDocumentUUID, + AUDIT_GET_BY_ORG_IDENTIFIER: getOrgAuditByOrgIdentifier, AUDIT_GET_LAST: getLastXChanges } diff --git a/src/controller/audit.controller/index.js b/src/controller/audit.controller/index.js index ba5b876a2..10da8f70f 100644 --- a/src/controller/audit.controller/index.js +++ b/src/controller/audit.controller/index.js @@ -27,15 +27,15 @@ router.get('/audit/org/document/:document_uuid', ) // Get audit by org identifier (Secretariat or Admin) -router.get('/audit/org/:target_uuid', +router.get('/audit/org/:org_identifier', mw.validateUser, mw.onlySecretariatOrAdmin, auditMw.parseGetParams, - controller.AUDIT_GET_BY_TARGET_UUID + controller.AUDIT_GET_BY_ORG_IDENTIFIER ) // Get last X changes (Secretariat or Org Admin) -router.get('/audit/org/:target_uuid/:number_of_changes', +router.get('/audit/org/:org_identifier/:number_of_changes', mw.onlySecretariatOrAdmin, mw.validateUser, auditMw.parseGetParams, diff --git a/src/controller/conversation.controller/conversation.controller.js b/src/controller/conversation.controller/conversation.controller.js index db9aab09a..0ed9a2c97 100644 --- a/src/controller/conversation.controller/conversation.controller.js +++ b/src/controller/conversation.controller/conversation.controller.js @@ -19,73 +19,27 @@ async function getAllConversations (req, res, next) { return res.status(200).json(response) } -async function getConversationsForOrg (req, res, next) { - const session = await mongoose.startSession() - - try { - session.startTransaction() - - const repo = req.ctx.repositories.getConversationRepository() - const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const requesterOrg = req.ctx.org - const targetOrgUUID = req.params.uuid - - // Make sure target org matches user org if not secretariat - const isSecretariat = await orgRepo.isSecretariatByShortName(requesterOrg, { session }) - const requesterOrgUUID = await orgRepo.getOrgUUID(requesterOrg, { session }) - if (!isSecretariat && (requesterOrgUUID !== targetOrgUUID)) { - return res.status(400).json({ message: 'User is not secretariat or admin for target org' }) - } - - // temporary measure to allow tests to work after fixing #920 - // tests required changing the global limit to force pagination - if (req.TEST_PAGINATOR_LIMIT) { - CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT - } - - const options = CONSTANTS.PAGINATOR_OPTIONS - options.sort = { posted_at: 'desc' } +async function getConversationsForTargetUUID (req, res, next) { + const repo = req.ctx.repositories.getConversationRepository() + const targetUUID = req.params.uuid - const response = await repo.getAllByTargetUUID(targetOrgUUID, options) - await session.commitTransaction() - return res.status(200).json(response) - } catch (err) { - if (session && session.inTransaction()) { - await session.abortTransaction() - } - next(err) - } finally { - if (session && session.id) { // Check if session is still valid before trying to end - try { - await session.endSession() - } catch (sessionEndError) { - logger.error({ uuid: req.ctx.uuid, message: 'Error ending session in finally block', error: sessionEndError }) - } - } - } + const response = await repo.getAllByTargetUUID(targetUUID) + return res.status(200).json(response) } -async function createConversationForOrg (req, res, next) { +async function createConversationForTargetUUID (req, res, next) { const session = await mongoose.startSession() try { session.startTransaction() const repo = req.ctx.repositories.getConversationRepository() - const orgRepo = req.ctx.repositories.getBaseOrgRepository() const userRepo = req.ctx.repositories.getBaseUserRepository() const requesterOrg = req.ctx.org const requesterUsername = req.ctx.user - const targetOrgUUID = req.params.uuid + const targetUUID = req.params.uuid const body = req.body - // Make sure target org matches user org if not secretariat - const isSecretariat = await orgRepo.isSecretariatByShortName(requesterOrg, { session }) - const requesterOrgUUID = await orgRepo.getOrgUUID(requesterOrg, { session }) - if (!isSecretariat && (requesterOrgUUID !== targetOrgUUID)) { - return res.status(400).json({ message: 'User is not secretariat or admin for target org' }) - } - const user = await userRepo.findOneByUsernameAndOrgShortname(requesterUsername, requesterOrg, { session }) if (!body.body) { @@ -93,10 +47,10 @@ async function createConversationForOrg (req, res, next) { } const conversationBody = { - target_uuid: targetOrgUUID, + target_uuid: targetUUID, author_id: user.UUID, author_name: [user.name.first, user.name.last].join(' '), - author_role: isSecretariat ? 'Secretariat' : 'Partner', + author_role: 'Secretariat', visibility: body.visibility ? body.visibility.toLowerCase() : 'private', body: body.body } @@ -129,20 +83,20 @@ async function createConversationForOrg (req, res, next) { async function updateMessage (req, res, next) { const repo = req.ctx.repositories.getConversationRepository() - const targetOrgUUID = req.params.uuid + const targetUUID = req.params.uuid const body = req.body if (!body.body) { return res.status(400).json({ message: 'Missing required field body' }) } - const result = await repo.updateConversation(body, targetOrgUUID) + const result = await repo.updateConversation(body, targetUUID) return res.status(200).json(result) } module.exports = { getAllConversations, - getConversationsForOrg, - createConversationForOrg, + getConversationsForTargetUUID, + createConversationForTargetUUID, updateMessage } diff --git a/src/controller/conversation.controller/index.js b/src/controller/conversation.controller/index.js index dfcb2b792..d2a8c44e0 100644 --- a/src/controller/conversation.controller/index.js +++ b/src/controller/conversation.controller/index.js @@ -15,33 +15,22 @@ router.get('/conversation', controller.getAllConversations ) -// Get conversations for all orgs - SEC only -router.get('/conversation/org', +// Get all conversations for target UUID - SEC only +router.get('/conversation/target/:uuid', mw.validateUser, mw.onlySecretariat, query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - controller.getAllConversations // TODO: for now, all conversations are targeted to orgs. Update this when conversations added for other objects + controller.getConversationsForTargetUUID ) -// Get conversations for org - SEC/ADMIN -router.get('/conversation/org/:uuid', +// Post conversation for target UUID - SEC only +router.post('/conversation/target/:uuid', mw.validateUser, - mw.onlySecretariatOrAdmin, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - param(['uuid']).isUUID(4), - controller.getConversationsForOrg -) - -// Post conversation for org - SEC/ADMIN -router.post('/conversation/org/:uuid', - mw.validateUser, - mw.onlySecretariatOrAdmin, + mw.onlySecretariat, param(['uuid']).isUUID(4), - controller.createConversationForOrg + controller.createConversationForTargetUUID ) // Update conversation message - SEC only diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 00ab0aba5..10d0ef5be 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -161,6 +161,8 @@ router.get('/registry/org/:shortname/users', mw.useRegistry(), mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), parseError, parseGetParams, @@ -237,6 +239,7 @@ router.get('/registry/org/:shortname/id_quota', mw.useRegistry(), mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.ORG_ID_QUOTA) @@ -311,6 +314,7 @@ router.get('/registry/org/:identifier', */ mw.useRegistry(), mw.validateUser, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.ORG_SINGLE @@ -394,27 +398,41 @@ router.get('/registry/org/:shortname/user/:username', mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.USER_SINGLE ) router.post('/registry/org', /* - #swagger.tags = ['Organization'] - #swagger.operationId = 'orgAll' - #swagger.summary = "Retrieves all organizations (accessible to Secretariat)" + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'orgCreateSingle' + #swagger.summary = "Creates an organization (accessible to Secretariat)" #swagger.description = "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

-

Secretariat: Retrieves information about all organizations

" +

Secretariat: Creates a new organization

" #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + anyOf: [ + { $ref: '../schemas/registry-org/SecretariatOrg.json' }, + { $ref: '../schemas/registry-org/CNAOrg.json' }, + { $ref: '../schemas/registry-org/ADPOrg.json' }, + { $ref: '../schemas/registry-org/BulkDownloadOrg.json' } + ] + } + } + } + } #swagger.responses[200] = { description: 'Returns information about all organizations, along with pagination fields if results span multiple pages of data', content: { @@ -469,6 +487,7 @@ router.post('/registry/org', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parsePostParams, parseError, controller.REGISTRY_CREATE_ORG @@ -491,7 +510,6 @@ router.put('/registry/org/:shortname', '#/components/parameters/newShortname', '#/components/parameters/active_roles_add', '#/components/parameters/active_roles_remove', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -733,10 +751,10 @@ router.put('/registry/org/:shortname/user/:username', mw.onlyOrgWithPartnerRole, query().custom((query) => { return mw.validateQueryParameterNames(query, ['active', 'new_username', 'org_short_name', 'name.first', 'name.last', 'name.middle', - 'name.suffix', 'active_roles.add', 'active_roles.remove', 'registry']) + 'name.suffix', 'active_roles.add', 'active_roles.remove']) }), query(['active', 'new_username', 'org_short_name', 'name.first', 'name.last', 'name.middle', - 'name.suffix', 'active_roles.add', 'active_roles.remove', 'registry']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + 'name.suffix', 'active_roles.add', 'active_roles.remove']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), query(['active']).optional().isBoolean({ loose: true }), @@ -847,7 +865,6 @@ router.get('/org',

Secretariat: Retrieves information about all organizations

" #swagger.parameters['$ref'] = [ '#/components/parameters/pageQuery', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -906,11 +923,10 @@ router.get('/org', } } */ - param(['registry']).optional().isBoolean(), mw.handleRegistryParameter, mw.validateUser, mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page', 'registry']) }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), parseError, @@ -930,7 +946,6 @@ router.post(

Secretariat: Creates an organization

" #swagger.parameters['$ref'] = [ - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -939,12 +954,7 @@ router.post( required: true, content: { 'application/json': { - schema: { - oneOf: [ - { $ref: '../schemas/org/create-org-request.json' }, - { $ref: '../schemas/registry-org/create-registry-org-request.json' } - ] - } + schema: { $ref: '../schemas/org/create-org-request.json' } } } } @@ -1004,8 +1014,13 @@ router.post( */ mw.validateUser, mw.onlySecretariat, - body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - body(['name']).isString().trim().notEmpty(), + body(['short_name']) + .isString().withMessage(errorMsgs.MUST_BE_STRING).trim() + .notEmpty().withMessage(errorMsgs.NOT_EMPTY) + .isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }).withMessage(errorMsgs.SHORTNAME_LENGTH), + body(['name']) + .isString().withMessage(errorMsgs.MUST_BE_STRING).trim() + .notEmpty().withMessage(errorMsgs.NOT_EMPTY), body(['authority.active_roles']).optional() .custom(isFlatStringArray) .customSanitizer(toUpperCaseArray) @@ -1029,7 +1044,6 @@ router.get(

Secretariat: Retrieves information about any organization

" #swagger.parameters['identifier'] = { description: 'The shortname or UUID of the organization' } #swagger.parameters['$ref'] = [ - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1038,11 +1052,7 @@ router.get( description: 'Returns the organization information', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/org/get-org-response.json' }, - { $ref: '../schemas/registry-org/get-registry-org-response.json' } - ] + schema: { $ref: '../schemas/org/get-org-response.json' } } } } @@ -1088,10 +1098,9 @@ router.get( } } */ - param(['registry']).optional().isBoolean(), - mw.handleRegistryParameter, mw.validateUser, param(['identifier']).isString().trim(), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.ORG_SINGLE @@ -1113,7 +1122,6 @@ router.put('/org/:shortname', '#/components/parameters/newShortname', '#/components/parameters/active_roles_add', '#/components/parameters/active_roles_remove', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1122,12 +1130,7 @@ router.put('/org/:shortname', description: 'Returns information about the organization updated', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/org/update-org-response.json' }, - { $ref: '../schemas/registry-org/update-registry-org-response.json' } - ] - } + schema: { $ref: '../schemas/org/update-org-response.json' } } } } @@ -1178,6 +1181,7 @@ router.put('/org/:shortname', parseError, parsePostParams, controller.ORG_UPDATE_SINGLE) + router.get('/org/:shortname/id_quota', /* #swagger.tags = ['Organization'] @@ -1191,7 +1195,6 @@ router.get('/org/:shortname/id_quota',

Secretariat: Retrieves the CVE ID quota for any organization

" #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } #swagger.parameters['$ref'] = [ - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1200,11 +1203,7 @@ router.get('/org/:shortname/id_quota', description: 'Returns the CVE ID quota for an organization', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/org/get-org-quota-response.json' }, - { $ref: '../schemas/registry-org/get-registry-org-quota-response.json' } - ] + schema: { $ref: '../schemas/org/get-org-quota-response.json' } } } } @@ -1250,9 +1249,9 @@ router.get('/org/:shortname/id_quota', } } */ - mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.ORG_ID_QUOTA) @@ -1270,7 +1269,6 @@ router.get('/org/:shortname/users', #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } #swagger.parameters['$ref'] = [ '#/components/parameters/pageQuery', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1279,12 +1277,7 @@ router.get('/org/:shortname/users', description: 'Returns all users for the organization, along with pagination fields if results span multiple pages of data', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/user/list-users-response.json' }, - { $ref: '../schemas/registry-user/list-registry-users-response.json' } - ] - } + schema: { $ref: '../schemas/user/list-users-response.json' } } } } @@ -1329,10 +1322,11 @@ router.get('/org/:shortname/users', } } */ - param(['registry']).optional().isBoolean(), mw.handleRegistryParameter, mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), parseError, parseGetParams, @@ -1351,7 +1345,6 @@ router.post('/org/:shortname/user',

Secretariat: Creates a user for any organization

" #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } #swagger.parameters['$ref'] = [ - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1418,6 +1411,7 @@ router.post('/org/:shortname/user', mw.onlySecretariatOrAdmin, mw.onlyOrgWithPartnerRole, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), body(['org_uuid']).optional().isString().trim(), body(['uuid']).optional().isString().trim(), body(['name.first']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_FIRSTNAME_LENGTH }).withMessage(errorMsgs.FIRSTNAME_LENGTH), @@ -1432,6 +1426,7 @@ router.post('/org/:shortname/user', parseError, parsePostParams, controller.USER_CREATE_SINGLE) + router.get('/org/:shortname/user/:username', /* #swagger.tags = ['Users'] @@ -1502,9 +1497,11 @@ router.get('/org/:shortname/user/:username', mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.USER_SINGLE) + router.put('/org/:shortname/user/:username', /* #swagger.tags = ['Users'] @@ -1529,7 +1526,6 @@ router.put('/org/:shortname/user/:username', '#/components/parameters/nameSuffix', '#/components/parameters/newUsername', '#/components/parameters/orgShortname', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1588,10 +1584,10 @@ router.put('/org/:shortname/user/:username', mw.onlyOrgWithPartnerRole, query().custom((query) => { return mw.validateQueryParameterNames(query, ['active', 'new_username', 'org_short_name', 'name.first', 'name.last', 'name.middle', - 'name.suffix', 'active_roles.add', 'active_roles.remove', 'registry']) + 'name.suffix', 'active_roles.add', 'active_roles.remove']) }), query(['active', 'new_username', 'org_short_name', 'name.first', 'name.last', 'name.middle', - 'name.suffix', 'active_roles.add', 'active_roles.remove', 'registry']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + 'name.suffix', 'active_roles.add', 'active_roles.remove']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), query(['active']).optional().isBoolean({ loose: true }), @@ -1613,6 +1609,7 @@ router.put('/org/:shortname/user/:username', parseError, parsePostParams, controller.USER_UPDATE_SINGLE) + router.put('/org/:shortname/user/:username/reset_secret', /* #swagger.tags = ['Users'] @@ -1685,6 +1682,7 @@ router.put('/org/:shortname/user/:username/reset_secret', mw.onlyOrgWithPartnerRole, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parsePostParams, controller.USER_RESET_SECRET) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index ba90751b8..32ff68af9 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -67,6 +67,10 @@ async function getOrg (req, res, next) { returnValue = await repo.getOrg(identifier, identifierIsUUID, { session }, !req.useRegistry) } catch (error) { await session.abortTransaction() + // Handle the specific error thrown by BaseOrgRepository.createOrg + if (error.message && error.message.includes('Unknown Org type requested')) { + return res.status(400).json({ message: error.message }) + } throw error } finally { await session.endSession() @@ -85,7 +89,7 @@ async function getOrg (req, res, next) { /** * Get the details of all users from an org given the specified shortname - * Called by GET /api/org/{shortname}/users + * Called by GET /api/registry/org/{shortname}/users, GET /api/org/{shortname}/users **/ async function getUsers (req, res, next) { try { @@ -118,7 +122,7 @@ async function getUsers (req, res, next) { return res.status(403).json(error.notSameOrgOrSecretariat()) } - const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, true) + const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, !req.useRegistry) logger.info({ uuid: req.ctx.uuid, message: `The users of ${orgShortName} organization were sent to the user.` }) return res.status(200).json(payload) @@ -129,7 +133,7 @@ async function getUsers (req, res, next) { /** * Get the details of a single user for the specified username - * Called by GET /api/org/{shortname}/user/{username} + * Called by GET /api/registry/org/{shortname}/user/{username}, GET /api/org/{shortname}/user/{username} **/ async function getUser (req, res, next) { try { @@ -234,9 +238,9 @@ async function registryCreateOrg (req, res, next) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) await session.abortTransaction() if (!Array.isArray(body?.authority) || body?.authority.some(item => typeof item !== 'string')) { - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) + return res.status(400).json({ error: 'BAD_INPUT', message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) } - return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) + return res.status(400).json({ error: 'BAD_INPUT', message: 'Parameters were invalid', errors: result.errors }) } // Check to see if the org already exists @@ -249,7 +253,8 @@ async function registryCreateOrg (req, res, next) { // If we get here, we know we are good to create const userRepo = req.ctx.repositories.getBaseUserRepository() const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) - returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, false, requestingUserUUID) + const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) + returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, false, requestingUserUUID, isSecretariat) await session.commitTransaction() logger.info({ @@ -293,7 +298,10 @@ async function createOrg (req, res, next) { await session.abortTransaction() return res.status(400).json(error.orgExists(body?.short_name)) } - returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, true) + const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) + const userRepo = req.ctx.repositories.getBaseUserRepository() + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, true, requestingUserUUID, isSecretariat) await session.commitTransaction() } catch (error) { @@ -366,7 +374,9 @@ async function registryUpdateOrg (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) - const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, false, requestingUserUUID) + const isSecretariat = await orgRepository.isSecretariatByShortName(req.ctx.org, { session }) + const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) + const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, false, requestingUserUUID, isAdmin, isSecretariat) responseMessage = { message: `${updatedOrg.short_name} organization was successfully updated.`, updated: updatedOrg } // Clarify message const payload = { action: 'update_org', change: `${updatedOrg.short_name} organization was successfully updated.`, org: updatedOrg } @@ -413,9 +423,13 @@ async function updateOrg (req, res, next) { return res.status(403).json(error.duplicateShortname(queryParametersJson.new_short_name)) } - const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, true) - const userRepo = req.ctx.repositories.getBaseUserRepository() + const isSecretariat = await orgRepository.isSecretariatByShortName(req.ctx.org, { session }) + const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + + const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, true, requestingUserUUID, isAdmin, isSecretariat) + responseMessage = { message: `${updatedOrg.short_name} organization was successfully updated.`, updated: updatedOrg } // Clarify message const payload = { action: 'update_org', change: `${updatedOrg.short_name} organization was successfully updated.`, org: updatedOrg } payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, updatedOrg.UUID) @@ -438,7 +452,7 @@ async function updateOrg (req, res, next) { /** * Creates a user only if the org exists and * the user does not exist for the specified shortname and username - * Called by POST /api/org/{shortname}/user + * Called by POST /api/registry/org/{shortname}/user, POST /api/org/{shortname}/user **/ async function createUser (req, res, next) { const session = await mongoose.startSession() @@ -447,6 +461,7 @@ async function createUser (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() const orgShortName = req.ctx.params.shortname + const constants = getConstants() let returnValue // Check to make sure Org Exists first @@ -472,6 +487,9 @@ async function createUser (req, res, next) { if (body?.role && typeof body?.role !== 'string') { return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) } + if (body?.role && !constants.USER_ROLES.includes(body?.role)) { + return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: `Role must be one of the following: ${constants.USER_ROLES}` }] }) + } if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) await session.abortTransaction() @@ -534,7 +552,7 @@ async function createUser (req, res, next) { /** * Updates a user only if the user exist for the specified username. * If no user exists, it does not create the user. - * Called by PUT /org/{shortname}/user/{username} + * Called by PUT /org/{shortname}/user/{username}, PUT /org/{shortname}/user/{username} **/ async function updateUser (req, res, next) { const session = await mongoose.startSession() diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 6e8cbcd0c..cd9d2333e 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -48,6 +48,18 @@ function validateCreateOrgParameters () { .isArray(), body(['root_or_tlr']).default(false) .isBoolean(), + body(['vulnerability_advisory_locations']) + .default([]) + .custom(isFlatStringArray), + body(['advisory_location_require_credentials']) + .default(false) + .isBoolean(), + body(['tl_root_start_date']) + .default(null) + .isDate(), + body(['is_cna_discussion_list']) + .default(false) + .isBoolean(), body( [ 'charter_or_scope', @@ -58,7 +70,10 @@ function validateCreateOrgParameters () { 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', - 'contact_info.website' + 'contact_info.website', + 'cna_role_type', + 'cna_country', + 'industry' ]) .default('') .isString(), @@ -119,7 +134,14 @@ function validateCreateOrgParameters () { 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.additional_contact_users', - 'contact_info.website') + 'contact_info.website', + 'cna_role_type', + 'cna_country', + 'vulnerability_advisory_locations', + 'advisory_location_require_credentials', + 'industry', + 'tl_root_start_date', + 'is_cna_discussion_list') ] } @@ -169,7 +191,7 @@ function validateUpdateOrgParameters () { const useRegistry = req.query.registry === 'true' const legacyParametersOnly = ['id_quota', 'name'] - const registryParametersOnly = ['hard_quota', 'long_name', 'cve_program_org_function', 'oversees', 'root_or_tlr', 'charter_or_scope', 'disclosure_policy', 'product_list'] + const registryParametersOnly = ['hard_quota', 'long_name', 'cve_program_org_function', 'oversees', 'root_or_tlr', 'charter_or_scope', 'disclosure_policy', 'product_list', 'cna_role_type', 'cna_country', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list'] const sharedParameters = ['new_short_name', 'active_roles.add', 'active_roles.remove', 'registry'] const allParameters = [ @@ -191,28 +213,40 @@ function validateUpdateOrgParameters () { if (useRegistry) { validations.push( - - query(['hard_quota']).optional().not().isArray().isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }).withMessage(errorMsgs.ID_QUOTA), + query(['hard_quota']) + .optional() + .not() + .isArray() + .isInt({ + min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, + max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max + }) + .withMessage(errorMsgs.ID_QUOTA), query(['long_name']).optional().isString().trim().notEmpty(), query(['oversees']).optional().isArray(), query(['root_or_tlr']).optional().isBoolean(), - query( - [ - 'cve_program_org_function', - 'charter_or_scope', - 'disclosure_policy', - 'product_list', - 'contact_info.poc', - 'contact_info.poc_email', - 'contact_info.poc_phone', - 'contact_info.org_email', - 'contact_info.website' - ]) + query([ + 'cve_program_org_function', + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'contact_info.poc', + 'contact_info.poc_email', + 'contact_info.poc_phone', + 'contact_info.org_email', + 'contact_info.website', + 'cna_role_type', + 'cna_country', + 'vulnerability_advisory_locations', + 'advisory_location_require_credentials', + 'industry', + 'tl_root_start_date', + 'is_cna_discussion_list' + ]) .optional() .isString(), ...isNotAllowedQuery(...legacyParametersOnly) // if we decide that we want to allow more, we can add them here. - ) } else { validations.push( @@ -273,10 +307,20 @@ function isUserRole (val) { function parsePostParams (req, res, next) { utils.reqCtxMapping(req, 'body', []) utils.reqCtxMapping(req, 'query', [ - 'new_short_name', 'name', 'id_quota', 'active', - 'active_roles.add', 'active_roles.remove', - 'new_username', 'org_short_name', - 'name.first', 'name.last', 'name.middle', 'name.suffix', 'long_name', 'cve_program_org_function', + 'new_short_name', + 'name', + 'id_quota', + 'active', + 'active_roles.add', + 'active_roles.remove', + 'new_username', + 'org_short_name', + 'name.first', + 'name.last', + 'name.middle', + 'name.suffix', + 'long_name', + 'cve_program_org_function', 'charter_or_scope', 'disclosure_policy', 'product_list', @@ -285,7 +329,16 @@ function parsePostParams (req, res, next) { 'contact_info.poc_phone', 'contact_info.org_email', 'hard_quota', - 'contact_info.website', 'root_or_tlr', 'oversees' + 'contact_info.website', + 'root_or_tlr', + 'oversees', + 'cna_role_type', + 'cna_country', + 'vulnerability_advisory_locations', + 'advisory_location_require_credentials', + 'industry', + 'tl_root_start_date', + 'is_cna_discussion_list' ]) utils.reqCtxMapping(req, 'params', ['shortname', 'username']) next() diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index 25bc038a1..36dad9aa7 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -213,7 +213,6 @@ router.post('/registryOrg', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, parseError, parsePostParams, controller.CREATE_ORG @@ -300,7 +299,6 @@ router.put('/registryOrg/:shortname', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, param(['shortname']).isString().trim(), parseError, parsePostParams, diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 638650b5d..a0f98e611 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose') const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') +const _ = require('lodash') const errors = require('./error') const error = new errors.RegistryOrgControllerError() const validateUUID = require('uuid').validate @@ -115,7 +116,11 @@ async function createOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getBaseOrgRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() const body = req.ctx.body + const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + let createdOrg // Do not allow the user to pass in a UUID @@ -125,7 +130,7 @@ async function createOrg (req, res, next) { try { session.startTransaction() - const result = repo.validateOrg(req.ctx.body, { session }) + const result = repo.validateOrg(body, { session }) if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) await session.abortTransaction() @@ -151,27 +156,45 @@ async function createOrg (req, res, next) { } // Create the org – repo.createOrg will handle field mapping - createdOrg = await repo.createOrg(body, { session, upsert: true }) + createdOrg = await repo.createOrg(body, { session, upsert: true }, false, requestingUserUUID, isSecretariat) await session.commitTransaction() } catch (createErr) { await session.abortTransaction() + if (createErr.message && createErr.message.includes('Unknown Org type requested')) { + return res.status(400).json({ message: createErr.message }) + } throw createErr } finally { await session.endSession() } - const responseMessage = { - message: `${body?.short_name} organization was successfully created.`, - created: createdOrg - } + let responseMessage + let payload + if (isSecretariat) { + responseMessage = { + message: `${body?.short_name} organization was successfully created.`, + created: createdOrg + } - const payload = { - action: 'create_org', - change: `${body?.short_name} organization was successfully created.`, - req_UUID: req.ctx.uuid, - org_UUID: createdOrg.UUID, - org: createdOrg + payload = { + action: 'create_org', + change: `${body?.short_name} organization was successfully created.`, + req_UUID: req.ctx.uuid, + org_UUID: createdOrg.UUID, + org: createdOrg + } + } else { + payload = { + action: 'create_review_org', + change: body?.short_name + ' was successfully requested to be Reviewed.', + req_UUID: req.ctx.uuid + } + + responseMessage = { + message: body?.short_name + ' was successfully received to be reviewed. By using Load ReviewObject data, you can check for a reply from the Secretariat about Joint Approval items.', + created: body?.shortName + } } logger.info(JSON.stringify(payload)) @@ -198,16 +221,42 @@ async function updateOrg (req, res, next) { const session = await mongoose.startSession() const shortName = req.ctx.params.shortname const repo = req.ctx.repositories.getBaseOrgRepository() - const body = req.ctx.body + const userRepo = req.ctx.repositories.getBaseUserRepository() + const conversationRepo = req.ctx.repositories.getConversationRepository() + const { conversation, ...body } = req.ctx.body let updatedOrg + let jointApprovalRequired try { session.startTransaction() + const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) + const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) + const requestingUser = await userRepo.findOneByUsernameAndOrgShortname(req.ctx.user, req.ctx.org, { session }) const org = await repo.findOneByShortName(shortName) + + // Edge Case: if a user has requested an org, but it is not approved yet, then we need to check to see if if there is a review org for the shortname request. + if (!org) { - logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated because it does not exist.' }) - await session.abortTransaction() - return res.status(404).json(error.orgDnePathParam(shortName)) + // resolve edge case + const reviewRepo = req.ctx.repositories.getReviewObjectRepository() + const reviewOrg = await reviewRepo.getOrgReviewObjectStandaloneByRequestedOrgShortname(shortName, { session }) + + // Eventually we should validate this, but this is a bit tricky. + if (reviewOrg) { + const updateResult = await reviewRepo.updateReviewOrgObject(body, reviewOrg.uuid, { session }) + if (updateResult) { + updatedOrg = reviewOrg + if (conversation && conversation.length) { + await conversationRepo.processConversationHistory(conversation, updateResult.uuid, requestingUser, isSecretariat, { session }) + } + await session.commitTransaction() + return res.status(200).json({ message: 'Review object updated successfully' }) + } + } else { + logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated because it does not exist.' }) + await session.abortTransaction() + return res.status(404).json(error.orgDnePathParam(shortName)) + } } const result = repo.validateOrg(body, { session }) @@ -227,7 +276,10 @@ async function updateOrg (req, res, next) { return res.status(400).json(error.duplicateShortname(body?.short_name)) } - updatedOrg = await repo.updateOrgFull(shortName, body, { session }) + updatedOrg = await repo.updateOrgFull(shortName, req.ctx.body, { session }, false, requestingUser.UUID, isAdmin, isSecretariat) + jointApprovalRequired = _.get(updatedOrg, 'joint_approval_required', false) + _.unset(updatedOrg, 'joint_approval_required') + await session.commitTransaction() } catch (updateErr) { await session.abortTransaction() @@ -236,20 +288,38 @@ async function updateOrg (req, res, next) { await session.endSession() } - const responseMessage = { - message: `${body?.short_name} organization was successfully updated.`, - updated: updatedOrg - } + if (jointApprovalRequired) { + const responseMessage = { + message: `${body?.short_name} organization was successfully updated, but joint approval is required for some fields. Check the ReviewObject for your org to check for a reply from the Secretariat about Joint Approval items.`, + updated: updatedOrg + } - const payload = { - action: 'update_registry_org', - change: body?.short_name + ' was successfully updated.', - req_UUID: req.ctx.uuid, - org_UUID: await repo.getOrgUUID(req.ctx.org), - org: updatedOrg + const payload = { + action: 'update_registry_org', + change: body?.short_name + 'organization was successfully updated, but joint approval is required for some fields. Check the ReviewObject for your org to check for a reply from the Secretariat about Joint Approval items.', + req_UUID: req.ctx.uuid, + org_UUID: await repo.getOrgUUID(req.ctx.org), + org: updatedOrg + } + + logger.info(JSON.stringify(payload)) + return res.status(200).json(responseMessage) + } else { + const responseMessage = { + message: `${body?.short_name} organization was successfully updated.`, + updated: updatedOrg + } + + const payload = { + action: 'update_registry_org', + change: body?.short_name + ' was successfully updated.', + req_UUID: req.ctx.uuid, + org_UUID: await repo.getOrgUUID(req.ctx.org), + org: updatedOrg + } + logger.info(JSON.stringify(payload)) + return res.status(200).json(responseMessage) } - logger.info(JSON.stringify(payload)) - return res.status(200).json(responseMessage) } catch (err) { next(err) } diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index e2283f589..325c9d107 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -15,7 +15,14 @@ function parsePostParams (req, res, next) { 'charter_or_scope', 'disclosure_policy', 'product_list', 'soft_quota', 'hard_quota', 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', - 'contact_info.admins', 'contact_info.org_email', 'contact_info.website' + 'contact_info.admins', 'contact_info.org_email', 'contact_info.website', + 'cna_role_type', + 'cna_country', + 'vulnerability_advisory_locations', + 'advisory_location_require_credentials', + 'industry', + 'tl_root_start_date', + 'is_cna_discussion_list' ]) next() } diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js index 55d650ae3..db97b684b 100644 --- a/src/controller/registry-user.controller/index.js +++ b/src/controller/registry-user.controller/index.js @@ -145,7 +145,7 @@ router.get('/registryUser/:identifier', controller.SINGLE_USER ) -router.post('/registryUser', +router.post('/registryUser/:shortname', /* #swagger.tags = ['Registry User'] #swagger.operationId = 'createRegistryUser' @@ -212,9 +212,6 @@ router.post('/registryUser', */ mw.validateUser, mw.onlySecretariat, - // mw.onlySecretariat, // TODO: permissions - // TODO: validation - // parseError, parsePostParams, controller.CREATE_USER ) diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index 7261bda22..2ce44cd53 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -1,16 +1,15 @@ -const argon2 = require('argon2') -const cryptoRandomString = require('crypto-random-string') -const uuid = require('uuid') +const mongoose = require('mongoose') const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') -const RegistryUser = require('../../model/registry-user') -const RegistryOrg = require('../../model/registry-org') const errors = require('../user.controller/error') const error = new errors.UserControllerError() async function getAllUsers (req, res, next) { try { const CONSTANTS = getConstants() + const session = await mongoose.startSession() + const repo = req.ctx.repositories.getBaseUserRepository() + let returnValue // temporary measure to allow tests to work after fixing #920 // tests required changing the global limit to force pagination @@ -21,27 +20,15 @@ async function getAllUsers (req, res, next) { const options = CONSTANTS.PAGINATOR_OPTIONS options.sort = { short_name: 'asc' } options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value - const repo = req.ctx.repositories.getRegistryUserRepository() - const agt = setAggregateUserObj({}) - const pg = await repo.aggregatePaginate(agt, options) - - await RegistryOrg.populateOrgAffiliations(pg.itemsList) - await RegistryOrg.populateCVEProgramOrgMembership(pg.itemsList) - - const payload = { users: pg.itemsList } - - if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { - payload.totalCount = pg.itemCount - payload.itemsPerPage = pg.itemsPerPage - payload.pageCount = pg.pageCount - payload.currentPage = pg.currentPage - payload.prevPage = pg.prevPage - payload.nextPage = pg.nextPage + try { + returnValue = await repo.getAllUsers(options) + } finally { + await session.endSession() } logger.info({ uuid: req.ctx.uuid, message: 'The user information was sent to the secretariat user.' }) - return res.status(200).json(payload) + return res.status(200).json(returnValue) } catch (err) { next(err) } @@ -49,13 +36,14 @@ async function getAllUsers (req, res, next) { async function getUser (req, res, next) { try { - const repo = req.ctx.repositories.getRegistryUserRepository() + const repo = req.ctx.repositories.getBaseUserRepository() const identifier = req.ctx.params.identifier - const agt = setAggregateUserObj({ UUID: identifier }) - let result = await repo.aggregate(agt) - result = result.length > 0 ? result[0] : null - logger.info({ uuid: req.ctx.uuid, message: identifier + ' user was sent to the user.', user: result }) + const result = await repo.findUserByUUID(identifier) + if (!result) { + logger.info({ uuid: req.ctx.uuid, message: identifier + 'user could not be found.' }) + return res.status(404).json(error.userDne(identifier)) + } return res.status(200).json(result) } catch (err) { next(err) @@ -63,68 +51,67 @@ async function getUser (req, res, next) { } async function createUser (req, res, next) { + const session = await mongoose.startSession() try { - // const requesterUsername = req.ctx.user - // const requesterShortName = req.ctx.org - const orgRepo = req.ctx.repositories.getOrgRepository() - const userRepo = req.ctx.repositories.getUserRepository() - const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() const body = req.ctx.body + const orgShortName = req.ctx.params.shortname + let returnValue - // Short circuit if UUID provided - const bodyKeys = Object.keys(body).map((k) => k.toLowerCase()) - if (bodyKeys.includes('uuid')) { + const orgUUID = await orgRepo.getOrgUUID(orgShortName) + if (!orgUUID) { + logger.info({ uuid: req.ctx.uuid, message: 'The user could not be created because ' + orgShortName + ' organization does not exist.' }) + return res.status(404).json(error.orgDnePathParam(orgShortName)) + } + + // Do not allow the user to pass in a UUID + if ((body?.UUID ?? null) || (body?.uuid ?? null)) { return res.status(400).json(error.uuidProvided('user')) } - // TODO: check if affiliated orgs and program orgs exist, and if their membership limit is reached + if ((body?.org_UUID ?? null) || (body?.org_uuid ?? null)) { + return res.status(400).json(error.uuidProvided('org')) + } + + try { + session.startTransaction() - const newUser = new RegistryUser() - Object.keys(body).map(k => k.toLowerCase()).forEach(k => { - if (k === 'user_id' || k === 'username') { - newUser.user_id = body[k] - } else if (k === 'name') { - newUser.name = { - first: '', - last: '', - middle: '', - suffix: '', - ...body.name - } - } else if (k === 'org_affiliations') { - // TODO: dedupe - } else if (k === 'cve_program_org_membership') { - // TODO: dedupe + const result = await userRepo.validateUser(body) + if (body?.role && typeof body?.role !== 'string') { + return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) + } + if (!result.isValid) { + logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) + await session.abortTransaction() + return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) } - }) - - // TODO: check that requesting user is admin of org for new user - newUser.UUID = uuid.v4() - const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }) - newUser.secret = await argon2.hash(randomKey) - newUser.last_active = null - newUser.deactivation_date = null + // Ask repo if user already exists + if (await userRepo.orgHasUser(orgShortName, body?.username, { session })) { + logger.info({ uuid: req.ctx.uuid, message: `${body?.username} user was not created because it already exists.` }) + await session.abortTransaction() + return res.status(400).json(error.userExists(body?.username)) + } - await registryUserRepo.updateByUUID(newUser.UUID, newUser, { upsert: true }) - const agt = setAggregateUserObj({ UUID: newUser.UUID }) - let result = await registryUserRepo.aggregate(agt) - result = result.length > 0 ? result[0] : null + const users = await userRepo.findUsersByOrgShortname(orgShortName, { session }) + if (users.length >= 100) { + await session.abortTransaction() + return res.status(400).json(error.userLimitReached()) + } - const payload = { - action: 'create_registry_user', - change: result.user_id + ' was successfully created.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - user: result + returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }) + await session.commitTransaction() + } catch (error) { + await session.abortTransaction() + throw error + } finally { + await session.endSession() } - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) - logger.info(JSON.stringify(payload)) - result.secret = randomKey const responseMessage = { - message: result.user_id + ' was successfully created.', - created: result + message: `${body?.user_id} + ' was successfully created.`, + created: returnValue } return res.status(200).json(responseMessage) @@ -134,55 +121,34 @@ async function createUser (req, res, next) { } async function updateUser (req, res, next) { - try { - // const username = req.ctx.params.username - // const shortName = req.ctx.params.shortname - const userUUID = req.ctx.params.identifier - const userRepo = req.ctx.repositories.getUserRepository() - const orgRepo = req.ctx.repositories.getOrgRepository() - const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() - // const orgUUID = await orgRepo.getOrgUUID(shortName) - // Check if requester is Admin of the designated user's org - - const user = await registryUserRepo.findOneByUUID(userUUID) - const newUser = new RegistryUser() - - // Sets the name values to what currently exists in the database, this ensures data is retained during partial name updates - newUser.name.first = user.name.first - newUser.name.last = user.name.last - newUser.name.middle = user.name.middle - newUser.name.suffix = user.name.suffix - - // TODO: check permissions - // Check to ensure that the user has the right permissions to edit the fields tha they are requesting to edit, and fail fast if they do not. - // if (Object.keys(req.ctx.query).length > 0 && Object.keys(req.ctx.query).some((key) => { return queryParameterPermissions[key] }) && !(isAdmin || isSecretariat)) { - // logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + requesterUsername + ' user is not Org Admin or Secretariat to modify these fields.' }) - // return res.status(403).json(error.notOrgAdminOrSecretariatUpdate()) - // } - - for (const k in req.ctx.query) { - const key = k.toLowerCase() + const session = await mongoose.startSession() + const userUUID = req.ctx.params.identifier + const userRepo = req.ctx.repositories.getBaseUserRepository() + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const body = req.ctx.body + let result - if (key === 'new_user_id') { - newUser.user_id = req.ctx.query.new_user_id - } else if (key === 'name.first') { - newUser.name.first = req.ctx.query['name.first'] - } else if (key === 'name.last') { - newUser.name.last = req.ctx.query['name.last'] - } else if (key === 'name.middle') { - newUser.name.middle = req.ctx.query['name.middle'] - } else if (key === 'name.suffix') { - newUser.name.suffix = req.ctx.query['name.suffix'] + try { + session.startTransaction() + try { + result = await userRepo.validateUser(body) + if (body?.role && typeof body?.role !== 'string') { + return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) } - - // TODO: process org affiliations and program org membership updates + if (!result.isValid) { + logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) + await session.abortTransaction() + return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) + } + await userRepo.updateUserFull(userUUID, body, { session }) + await session.commitTransaction() + } catch (error) { + await session.abortTransaction() + throw error + } finally { + await session.endSession() } - await registryUserRepo.updateByUUID(userUUID, newUser) - const agt = setAggregateUserObj({ UUID: userUUID }) - let result = await registryUserRepo.aggregate(agt) - result = result.length > 0 ? result[0] : null - const payload = { action: 'update_registry_user', change: result.user_id + ' was successfully updated.', @@ -242,29 +208,6 @@ async function deleteUser (req, res, next) { } } -function setAggregateUserObj (query) { - return [ - { - $match: query - }, - { - $project: { - _id: false, - UUID: true, - user_id: true, - name: true, - org_affiliations: true, - cve_program_org_membership: true, - created: true, - created_by: true, - last_updated: true, - deactivation_date: true, - last_active: true - } - } - ] -} - module.exports = { ALL_USERS: getAllUsers, SINGLE_USER: getUser, diff --git a/src/controller/review-object.controller/index.js b/src/controller/review-object.controller/index.js index e13cb5b38..8c2b65a08 100644 --- a/src/controller/review-object.controller/index.js +++ b/src/controller/review-object.controller/index.js @@ -2,9 +2,11 @@ const router = require('express').Router() const controller = require('./review-object.controller') const mw = require('../../middleware/middleware') +router.get('/review/byUUID/:uuid', mw.useRegistry(), mw.validateUser, mw.onlySecretariatOrAdmin, controller.getReviewObjectByUUID) router.get('/review/org/:identifier', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getReviewObjectByOrgIdentifier) router.get('/review/orgs', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getAllReviewObjects) router.put('/review/org/:uuid', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.updateReviewObjectByReviewUUID) +router.put('/review/org/:uuid/approve', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.approveReviewObject) router.post('/review/org/', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.createReviewObject) module.exports = router diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index 3ef73148f..44717797e 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -1,7 +1,11 @@ const validateUUID = require('uuid').validate +const mongoose = require('mongoose') + async function getReviewObjectByOrgIdentifier (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org) const identifier = req.params.identifier const identifierIsUUID = validateUUID(identifier) if (!identifier) { @@ -10,9 +14,9 @@ async function getReviewObjectByOrgIdentifier (req, res, next) { let value // We may want this to be something different, but for now we are just testing if (identifierIsUUID) { - value = await repo.getOrgReviewObjectByOrgUUID(identifier) + value = await repo.getOrgReviewObjectByOrgUUID(identifier, isSecretariat) } else { - value = await repo.getOrgReviewObjectByOrgShortname(identifier) + value = await repo.getOrgReviewObjectByOrgShortname(identifier, isSecretariat) } if (!value) { return res.status(404).json({ message: 'Review Object does not exist' }) @@ -20,19 +24,54 @@ async function getReviewObjectByOrgIdentifier (req, res, next) { return res.status(200).json(value) } +async function getReviewObjectByUUID (req, res, next) { + const repo = req.ctx.repositories.getReviewObjectRepository() + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org) + const UUID = req.params.uuid + const value = await repo.findOneByUUIDWithConversation(UUID, isSecretariat) + return res.status(200).json(value) +} + async function getAllReviewObjects (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() const value = await repo.getAllReviewObjects() return res.status(200).json(value) } +async function approveReviewObject (req, res, next) { + const repo = req.ctx.repositories.getReviewObjectRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() + const UUID = req.params.uuid + const session = await mongoose.startSession() + let value + + try { + session.startTransaction() + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + + value = await repo.approveReviewOrgObject(UUID, requestingUserUUID, { session }) + await session.commitTransaction() + } catch (updateErr) { + await session.abortTransaction() + throw updateErr + } finally { + await session.endSession() + } + + if (!value) { + return res.status(404).json({ message: `No review object found with UUID ${UUID}` }) + } + return res.status(200).json(value) +} + async function updateReviewObjectByReviewUUID (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() const UUID = req.params.uuid const orgRepo = req.ctx.repositories.getBaseOrgRepository() const body = req.body - const result = orgRepo.validateOrg(body.new_review_data) + const result = orgRepo.validateOrg(body) if (!result.isValid) { return res.status(400).json({ message: 'Invalid new_review_data', errors: result.errors }) } @@ -47,27 +86,8 @@ async function updateReviewObjectByReviewUUID (req, res, next) { async function createReviewObject (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() - const orgRepo = req.ctx.repositories.getBaseOrgRepository() const body = req.body - if (body.uuid) { - return res.status(400).json({ message: 'Do not pass in a uuid key when creating a review object' }) - } - - if (!body.target_object_uuid) { - return res.status(400).json({ message: 'Missing required field target_object_uuid' }) - } - - if (!body.new_review_data) { - return res.status(400).json({ message: 'Missing required field new_review_data' }) - } - - // Validate the data going into "new_review_data" - const result = orgRepo.validateOrg(body.new_review_data) - if (!result.isValid) { - return res.status(400).json({ message: 'Invalid new_review_data', errors: result.errors }) - } - const value = await repo.createReviewOrgObject(body) if (!value) { @@ -77,7 +97,9 @@ async function createReviewObject (req, res, next) { } module.exports = { getReviewObjectByOrgIdentifier, + getReviewObjectByUUID, getAllReviewObjects, updateReviewObjectByReviewUUID, - createReviewObject + createReviewObject, + approveReviewObject } diff --git a/src/controller/user.controller/user.controller.js b/src/controller/user.controller/user.controller.js index 2bad29144..e66edf0ce 100644 --- a/src/controller/user.controller/user.controller.js +++ b/src/controller/user.controller/user.controller.js @@ -31,19 +31,6 @@ async function getAllUsers (req, res, next) { await session.endSession() } - /* const agt = isRegistry ? setAggregateRegistryUserObj({}) : setAggregateUserObj({}) - const pg = await repo.aggregatePaginate(agt, options) - const payload = { users: pg.itemsList } - - if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { - payload.totalCount = pg.itemCount - payload.itemsPerPage = pg.itemsPerPage - payload.pageCount = pg.pageCount - payload.currentPage = pg.currentPage - payload.prevPage = pg.prevPage - payload.nextPage = pg.nextPage - } */ - logger.info({ uuid: req.ctx.uuid, message: 'The user information was sent to the secretariat user.' }) return res.status(200).json(returnValue) } catch (err) { diff --git a/src/middleware/errorMessages.js b/src/middleware/errorMessages.js index c7aa7db12..7ed28d794 100644 --- a/src/middleware/errorMessages.js +++ b/src/middleware/errorMessages.js @@ -3,6 +3,9 @@ module.exports = { ORG_ROLES: 'Invalid role. Valid roles are CNA, SECRETARIAT', USER_ROLES: 'Invalid role. Valid role is ADMIN', + NOT_EMPTY: 'Value must not be empty', + SHORTNAME_LENGTH: 'Value must be between 2 and 20 characters in length.', + MUST_BE_STRING: 'Value must be a string', ID_QUOTA: 'The id_quota does not comply with CVE id quota limitations', ID_STATES: 'Invalid CVE ID state. Valid states are: RESERVED, PUBLISHED, REJECTED', ID_MODIFY_STATES: 'Invalid CVE ID state. Valid states are: RESERVED, REJECTED', diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 83d8c8a66..c07ebab71 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -547,6 +547,27 @@ function containsNoInvalidCharacters (val) { return true } +/** + * Middleware factory that rejects any keys in the request body + * that are not listed in the allowedKeys array. + * + * @param {Array} allowedKeys - List of permitted keys in req.body + * @returns {function} Express middleware + */ +function rejectUnexpectedKeys (allowedKeys) { + return (req, res, next) => { + const bodyKeys = Object.keys(req.body || {}) + const unexpected = bodyKeys.filter(k => !allowedKeys.includes(k)) + if (unexpected.length > 0) { + return res.status(400).json({ + error: 'Unexpected keys in request body', + unexpected + }) + } + next() + } +} + module.exports = { setCacheControl, optionallyValidateUser, @@ -572,5 +593,6 @@ module.exports = { toUpperCaseArray, toLowerCaseArray, containsNoInvalidCharacters, - trimJSONWhitespace + trimJSONWhitespace, + rejectUnexpectedKeys } diff --git a/src/middleware/schemas/ADPOrg.json b/src/middleware/schemas/ADPOrg.json index fd0998ff0..7979d1f55 100644 --- a/src/middleware/schemas/ADPOrg.json +++ b/src/middleware/schemas/ADPOrg.json @@ -9,7 +9,7 @@ { "properties": { "authority": { - "const": "ADP" + "const": ["ADP"] } } } diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json index fdabde496..7ce5aa663 100644 --- a/src/middleware/schemas/BaseOrg.json +++ b/src/middleware/schemas/BaseOrg.json @@ -39,6 +39,11 @@ "discriminator": { "description": "Discriminator key used by Mongoose for type inheritance", "type": "string" + }, + "timestamp": { + "description": "Date/time format based on RFC3339 and ISO ISO8601, with an optional timezone in the format 'yyyy-MM-ddTHH:mm:ss[+-]ZH:ZM'. If timezone offset is not given, GMT (+00:00) is assumed.", + "pattern": "^(((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)|(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))|(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))|(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30)))T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "type": "string" } }, "properties": { @@ -62,7 +67,11 @@ } }, "authority": { - "$ref": "#/definitions/authority" + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/authority" + } }, "root_or_tlr": { "type": "boolean" @@ -114,9 +123,35 @@ } }, "additionalProperties": false + }, + "cna_role_type": { + "type": "string" + }, + "cna_country": { + "type": "string" + }, + "vulnerability_advisory_locations": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "advisory_location_require_credentials": { + "type": "boolean" + }, + "industry": { + "type": "string" + }, + "tl_root_start_date": { + "$ref": "#/definitions/timestamp" + }, + "is_cna_discussion_list": { + "type": "boolean" } }, "required": [ - "short_name" + "short_name", + "long_name" ] } \ No newline at end of file diff --git a/src/middleware/schemas/BaseUser.json b/src/middleware/schemas/BaseUser.json index 70c898adc..2c1bf93de 100644 --- a/src/middleware/schemas/BaseUser.json +++ b/src/middleware/schemas/BaseUser.json @@ -57,11 +57,6 @@ "UUID": { "$ref": "#/definitions/uuidType" }, - "role": { - "description": "Users permissions, Like ADMIN", - "type": "string", - "enum": ["ADMIN"] - }, "status": { "description": "User status: 'active' or 'inactive'", "type": "string", diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json index f29c6336b..ada140853 100644 --- a/src/middleware/schemas/BulkDownloadOrg.json +++ b/src/middleware/schemas/BulkDownloadOrg.json @@ -5,11 +5,11 @@ "title": "CVE Bulk Download Organization", "description": "Schema for a CVE Bulk Download Organization", "allOf": [ - { "$ref": "BaseOrg" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { - "const": "BULK_DOWNLOAD" + "const": ["BULK_DOWNLOAD"] } } } diff --git a/src/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json index 274e61823..c1188c8c4 100644 --- a/src/middleware/schemas/CNAOrg.json +++ b/src/middleware/schemas/CNAOrg.json @@ -9,7 +9,7 @@ { "properties": { "authority": { - "const": "CNA" + "const": ["CNA"] }, "oversees": { "type": "array", diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json index 7dcb77975..4e658b571 100644 --- a/src/middleware/schemas/SecretariatOrg.json +++ b/src/middleware/schemas/SecretariatOrg.json @@ -9,7 +9,7 @@ { "properties": { "authority": { - "const": "SECRETARIAT" + "const": ["SECRETARIAT"] }, "oversees": { "type": "array", diff --git a/src/model/adporg.js b/src/model/adporg.js index 7932626c0..f5efa867c 100644 --- a/src/model/adporg.js +++ b/src/model/adporg.js @@ -5,13 +5,18 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) const AdpOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/ADPOrg.json')) -const ajv = new Ajv({ allErrors: false }) +const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) const validate = ajv.compile(AdpOrgSchema) -const schema = {} +// Hard and soft quotas should be retained if something was a cna, then became an adp, then back to cna +// In general, this should never happen, but we have a test case for it, so I want to make sure it works as expected. +const schema = { + hard_quota: Number, + soft_quota: Number +} const options = { discriminatorKey: 'kind' } const ADPSchema = new mongoose.Schema(schema, options) diff --git a/src/model/baseorg.js b/src/model/baseorg.js index e1d9c1c42..33edd6cf3 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -23,6 +23,13 @@ const schema = { org_email: String, website: String }, + cna_role_type: String, + cna_country: String, + vulnerability_advisory_locations: [String], + advisory_location_require_credentials: Boolean, + industry: String, + tl_root_start_date: Date, + is_cna_discussion_list: Boolean, in_use: Boolean, created: Date, last_updated: Date diff --git a/src/model/bulkdownloadorg.js b/src/model/bulkdownloadorg.js index 0acba5eeb..e196b5ff3 100644 --- a/src/model/bulkdownloadorg.js +++ b/src/model/bulkdownloadorg.js @@ -5,7 +5,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BulkDownloadOrg.json')) -const ajv = new Ajv({ allErrors: false }) +const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/cnaorg.js b/src/model/cnaorg.js index df1f3ca3e..ab17599c9 100644 --- a/src/model/cnaorg.js +++ b/src/model/cnaorg.js @@ -5,7 +5,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) const CnaOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/CNAOrg.json')) -const ajv = new Ajv({ allErrors: false }) +const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/registry-org.js b/src/model/registry-org.js deleted file mode 100644 index 4e44f491a..000000000 --- a/src/model/registry-org.js +++ /dev/null @@ -1,119 +0,0 @@ -const mongoose = require('mongoose') -const aggregatePaginate = require('mongoose-aggregate-paginate-v2') -const MongoPaging = require('mongo-cursor-pagination') - -const schema = { - _id: false, - UUID: String, - long_name: String, - short_name: String, - aliases: [String], - cve_program_org_function: { - type: String, - enum: ['Top Level Root', 'Root', 'CNA', 'CNA-LR', 'Secretariat', 'Board', 'AWG', 'TWG', 'SPWG', 'Bulk Download', 'ADP'] - }, - authority: { - active_roles: [String] - }, - reports_to: String, - oversees: [String], - root_or_tlr: Boolean, - users: [String], - charter_or_scope: String, - disclosure_policy: String, - product_list: String, - soft_quota: Number, - hard_quota: Number, - contact_info: { - additional_contact_users: [String], - poc: String, - poc_email: String, - poc_phone: String, - admins: [String], - org_email: String, - website: String - }, - in_use: Boolean, - created: Date, - last_updated: Date -} - -const orgPrivate = '-_id -soft_quota -hard_quota -contact_info.admins -in_use -created -last_updated -__v' -// const orgSecretariat = '' -const RegistryOrgSchema = new mongoose.Schema(schema, { collection: 'RegistryOrg', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }) - -RegistryOrgSchema.query.byShortName = function (shortName) { - return this.where({ short_name: shortName }) -} - -RegistryOrgSchema.query.byUUID = function (uuid) { - return this.where({ UUID: uuid }) -} - -RegistryOrgSchema.statics.populateOverseesAndReportsTo = async function (items) { // Assuming the model name is 'RegistryOrg' - for (const item of items) { - if (item.oversees.length > 0) { - const populatedOversees = await Promise.all( - item.oversees.map(async (uuid) => { - const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate) - return org ? org.toObject() : uuid // Return the org object if found, otherwise return the UUID - }) - ) - item.oversees = populatedOversees - } - if (item.reports_to) { - const org = await RegistryOrg.findOne({ UUID: item.reports_to }).select(orgPrivate) - item.reports_to = org ? org.toObject() : item.reports_to // Return the org object if found, otherwise return the UUID - } - } - - return this -} - -RegistryOrgSchema.statics.populateOrgAffiliations = async function (items) { // Assuming the model name is 'RegistryOrg' - for (const item of items) { - if (item.org_affiliations.length > 0) { - const populatedOrgs = await Promise.all( - item.org_affiliations.map(async ({ org_id: uuid, ...orgMeta }) => { - const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate) - return { - org: org ? org.toObject() : uuid, // Return the org object if found, otherwise return the UUID - ...orgMeta - } - }) - ) - item.org_affiliations = populatedOrgs - } - } - - return this -} - -RegistryOrgSchema.statics.populateCVEProgramOrgMembership = async function (items) { // Assuming the model name is 'RegistryOrg' - for (const item of items) { - if (item.cve_program_org_membership.length > 0) { - const populatedOrgs = await Promise.all( - item.cve_program_org_membership.map(async ({ program_org: uuid, ...orgMeta }) => { - const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate) - return { - org: org ? org.toObject() : uuid, // Return the org object if found, otherwise return the UUID - ...orgMeta - } - }) - ) - item.cve_program_org_membership = populatedOrgs - } - } - - return this -} - -RegistryOrgSchema.index({ UUID: 1 }) -RegistryOrgSchema.index({ 'authority.active_roles': 1 }) - -RegistryOrgSchema.plugin(aggregatePaginate) - -// Cursor pagination -RegistryOrgSchema.plugin(MongoPaging.mongoosePlugin) -const RegistryOrg = mongoose.model('RegistryOrg', RegistryOrgSchema) -module.exports = RegistryOrg diff --git a/src/model/registry-user.js b/src/model/registry-user.js deleted file mode 100644 index 8029a3775..000000000 --- a/src/model/registry-user.js +++ /dev/null @@ -1,111 +0,0 @@ -const mongoose = require('mongoose') -const aggregatePaginate = require('mongoose-aggregate-paginate-v2') -const MongoPaging = require('mongo-cursor-pagination') - -const schema = { - _id: false, - UUID: String, - user_id: String, - secret: String, - name: { - first: String, - last: String, - middle: String, - suffix: String - }, - org_affiliations: [{ - org_id: String, - email: String, - phone: String - }], - cve_program_org_membership: [{ - program_org: String, - roles: { - type: [String], - enum: ['Chair', 'Member', 'Admin'] - }, - status: { - type: String, - enum: ['active', 'inactive'] - } - }], - created: Date, - created_by: String, - last_updated: Date, - deactivation_date: Date, - last_active: Date -} - -const userPrivate = '-secret -_id -org_affiliations._id -cve_program_org_membership._id -created_by -created -last_updated -last_active -__v' -// const userSecretariat = '-secret' -const RegistryUserSchema = new mongoose.Schema(schema, { collection: 'RegistryUser', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }) - -RegistryUserSchema.query.byUserID = function (userID) { - return this.where({ user_id: userID }) -} - -RegistryUserSchema.query.byUUID = function (uuid) { - return this.where({ UUID: uuid }) -} - -RegistryUserSchema.query.byUserIdAndOrgUUID = function (userId, orgUUID) { - return this.where({ user_id: userId, 'org_affiliations.org_id': orgUUID }) -} - -RegistryUserSchema.statics.populateAdmins = async function (items) { // Assuming the model name is 'RegistryUser' - for (const item of items) { - if (item.contact_info && item.contact_info.admins && item.contact_info.admins.length > 0) { - const populatedAdmins = await Promise.all( - item.contact_info.admins.map(async (uuid) => { - const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields - return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID - }) - ) - item.contact_info.admins = populatedAdmins - } - } - - return this -} - -RegistryUserSchema.statics.populateUsers = async function (items) { // Assuming the model name is 'RegistryUser' - for (const item of items) { - if (item.users && item.users.length > 0) { - const populatedUsers = await Promise.all( - item.users.map(async (uuid) => { - const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields - return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID - }) - ) - item.users = populatedUsers - } - } - - return this -} - -RegistryUserSchema.statics.populateAdditionalContactUsers = async function (items) { // Assuming the model name is 'RegistryUser' - for (const item of items) { - if (item.contact_info && item.contact_info.additional_contact_users && item.contact_info.additional_contact_users.length > 0) { - const populatedUsers = await Promise.all( - item.contact_info.additional_contact_users.map(async (uuid) => { - const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields - return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID - }) - ) - item.users = populatedUsers - } - } - - return this -} - -RegistryUserSchema.index({ UUID: 1 }) -RegistryUserSchema.index({ user_id: 1 }) - -RegistryUserSchema.plugin(aggregatePaginate) - -// Cursor pagination -RegistryUserSchema.plugin(MongoPaging.mongoosePlugin) -const RegistryUser = mongoose.model('RegistryUser-Old', RegistryUserSchema) -module.exports = RegistryUser diff --git a/src/model/secretariatorg.js b/src/model/secretariatorg.js index 446fb0ca6..127d236a6 100644 --- a/src/model/secretariatorg.js +++ b/src/model/secretariatorg.js @@ -5,7 +5,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) const SecretariatOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/SecretariatOrg.json')) -const ajv = new Ajv({ allErrors: false }) +const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/repositories/auditRepository.js b/src/repositories/auditRepository.js index d79578d4a..1beabdbca 100644 --- a/src/repositories/auditRepository.js +++ b/src/repositories/auditRepository.js @@ -1,5 +1,6 @@ const Audit = require('../model/audit') const BaseRepository = require('./baseRepository') +const BaseOrgRepository = require('./baseOrgRepository') const uuid = require('uuid') class AuditRepository extends BaseRepository { @@ -13,49 +14,52 @@ class AuditRepository extends BaseRepository { return validateObject } - /** - * Create a new audit document - */ - async createAuditDocument (data, options = {}) { - const auditData = { - uuid: uuid.v4(), - target_uuid: data.target_uuid, - history: data.history || [] - } - - const audit = new Audit(auditData) - const result = await audit.save(options) - return result.toObject() - } - /** * Append a new entry to the audit history * Creates document if it doesn't exist */ - async appendToAuditHistory (targetUUID, auditObject, changeAuthor, options = {}) { + async appendToAuditHistoryForOrg (targetUUID, auditObject, changeAuthor, options = {}) { const historyEntry = { timestamp: new Date(), audit_object: auditObject, change_author: changeAuthor } + try { // Try to find existing document - let audit = await Audit.findOne({ target_uuid: targetUUID }) + let audit = await this.findOneByTargetUUID(targetUUID, options) - if (!audit) { + if (!audit) { // Create new document if doesn't exist - audit = new Audit({ - uuid: uuid.v4(), - target_uuid: targetUUID, - history: [historyEntry] - }) - } else { + // Assuming 'uuid' is available for generating a new UUID + audit = new Audit({ + uuid: uuid.v4(), + target_uuid: targetUUID, + history: [historyEntry] + }) + } else { // Append to existing history - audit.history.push(historyEntry) + audit.history.push(historyEntry) + } + + const result = await audit.save(options) + return result.toObject() + } catch (error) { + throw new Error('Failed to save audit history entry.') } + } - const result = await audit.save(options) - return result.toObject() + /** + * Find audit document by target UUID + */ + async findOneByOrgShortname (orgShortName, options = {}) { + const baseOrgRepository = new BaseOrgRepository() + const org = await baseOrgRepository.findOneByShortName(orgShortName) + if (!org) { + return null + } + const query = { target_uuid: org.UUID } + return this.collection.findOne(query, null, options) } /** @@ -63,7 +67,8 @@ class AuditRepository extends BaseRepository { */ async findOneByTargetUUID (targetUUID, options = {}) { const query = { target_uuid: targetUUID } - return this.collection.findOne(query, null, options) + const auditObject = await Audit.findOne(query, null, options) + return auditObject } /** diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 04c470e98..30945fe65 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -9,6 +9,7 @@ const uuid = require('uuid') const _ = require('lodash') const BaseOrg = require('../model/baseorg') const AuditRepository = require('./auditRepository') +const ConversationRepository = require('./conversationRepository') const getConstants = require('../constants').getConstants function setAggregateOrgObj (query) { @@ -34,6 +35,12 @@ function setAggregateRegistryOrgObj (query) { return [ { $match: query + }, + { + $project: { + _id: false, + __t: false + } } ] } @@ -167,7 +174,7 @@ class BaseOrgRepository extends BaseRepository { * @returns {Promise} A promise that resolves to a plain JavaScript object representing the newly created organization. The format of the returned object (legacy or registry) is determined by the `isLegacyObject` parameter. The object is stripped of internal properties and empty values. * @throws {string} Throws an error if the organization's authority role is not 'SECRETARIAT' or 'CNA'. */ - async createOrg (incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null) { + async createOrg (incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null, isSecretariat = false) { const { deepRemoveEmpty } = require('../utils/utils') const OrgRepository = require('./orgRepository') const CONSTANTS = getConstants() @@ -177,6 +184,8 @@ class BaseOrgRepository extends BaseRepository { let legacyObject = null let registryObject = null const legacyOrgRepo = new OrgRepository() + const ReviewObjectRepository = require('./reviewObjectRepository') + const reviewObjectRepo = new ReviewObjectRepository() // generate a shared uuid const sharedUUID = uuid.v4() @@ -205,13 +214,18 @@ class BaseOrgRepository extends BaseRepository { // Figure out why this is not working.... // registryObjectRaw = _.omitBy(registryObjectRaw, value => _.isNil(value) || _.isEmpty(value)) + // For all of these writes, if we are a secretariat, then we can write directly to the database, otherwise, we write to the review objects // Write - use org type specific model if (registryObjectRaw.authority.includes('SECRETARIAT')) { // Write // testing: registryObjectRaw.authority = 'SECRETARIAT' const SecretariatObjectToSave = new SecretariatOrgModel(registryObjectRaw) - registryObject = await SecretariatObjectToSave.save(options) + if (isSecretariat) { + registryObject = await SecretariatObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, { options }) + } } else if (registryObjectRaw.authority.includes('CNA')) { // A special case, we should make sure we have the default quota if it is not set if (!registryObjectRaw.hard_quota) { @@ -220,32 +234,43 @@ class BaseOrgRepository extends BaseRepository { } // Write const CNAObjectToSave = new CNAOrgModel(registryObjectRaw) - registryObject = await CNAObjectToSave.save(options) + if (isSecretariat) { + registryObject = await CNAObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, { options }) + } } else if (registryObjectRaw.authority.includes('ADP')) { registryObjectRaw.hard_quota = 0 const adpObjectToSave = new ADPOrgModel(registryObjectRaw) - registryObject = await adpObjectToSave.save(options) + if (isSecretariat) { + registryObject = await adpObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, { options }) + } } else if (registryObjectRaw.authority.includes('BULK_DOWNLOAD')) { registryObjectRaw.hard_quota = 0 const bulkDownloadObjectToSave = new BulkDownloadModel(registryObjectRaw) - registryObject = await bulkDownloadObjectToSave.save(options) + if (isSecretariat) { + registryObject = await bulkDownloadObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, { options }) + } } else { - // eslint-disable-next-line no-throw-literal - throw 'dave you screwed up' + // Throw an Error instance so callers can catch and handle it properly + throw new Error("Unknown Org type requested. Please use either 'SECRETARIAT', 'CNA', 'ADP', or 'BULK_DOWNLOAD' as the authority role.") } - // ADD AUDIT ENTRY AUTOMATICALLY for the registry object. At this point permissions and object validation have been done. + // ADD AUDIT ENTRY AUTOMATICALLY for the registry object if (requestingUserUUID) { try { const auditRepo = new AuditRepository() - await auditRepo.appendToAuditHistory( + await auditRepo.appendToAuditHistoryForOrg( registryObjectRaw.UUID, registryObjectRaw, requestingUserUUID, options ) } catch (auditError) { - // Don't fail the transaction if audit fails - just log it } } @@ -261,7 +286,7 @@ class BaseOrgRepository extends BaseRepository { if ( legacyObjectRaw.authority.active_roles.length === 1 && ( legacyObjectRaw.authority.active_roles[0] === 'ADP' || - legacyObjectRaw.authority.active_roles[0] === 'BULK_DOWNLOAD') + legacyObjectRaw.authority.active_roles[0] === 'BULK_DOWNLOAD') ) { // ADPs have quota of 0 _.set(legacyObjectRaw, 'policies.id_quota', 0) @@ -269,7 +294,14 @@ class BaseOrgRepository extends BaseRepository { // The legacy way of doing this, the way this is written under the hood there is no other way // This await does not return a value, even though there is a return in it. :shrugg: - await legacyOrgRepo.updateByOrgUUID(sharedUUID, legacyObjectRaw, options) + if (isSecretariat) { + await legacyOrgRepo.updateByOrgUUID(sharedUUID, legacyObjectRaw, options) + } + + // If we are not a secretariat, then we need to return the uuid of the review object. + if (!isSecretariat) { + return {} + } if (isLegacyObject) { // This gets us the mongoose object that has all the right data in it, the "legacyObjectRaw" is the custom JSON we are sending. NOT the post written object. @@ -317,64 +349,161 @@ class BaseOrgRepository extends BaseRepository { * @param {string} [incomingParameters.contact_info.poc_phone] - The primary point of contact's phone number. (Registry only) * @param {string} [incomingParameters.contact_info.org_email] - The general organization email address. (Registry only) * @param {string} [incomingParameters.contact_info.website] - The organization's website URL. (Registry only) + * @param {string} [incomingParameters.cna_role_type] - (Registry only) + * @param {string} [incomingParameters.cna_country] - (Registry only) + * @param {string[]} [incomingParameters.vulnerability_advisory_locations] - (Registry only) + * @param {boolean} [incomingParameters.advisory_location_require_credentials] - (Registry only) + * @param {string} [incomingParameters.industry] - (Registry only) + * @param {string} [incomingParameters.tl_root_start_date] - (Registry only) + * @param {boolean} [incomingParameters.is_cna_discussion_list] - (Registry only) * @param {object} [options={}] - Optional settings for the repository query. * @param {boolean} [isLegacyObject=false] - If true, the function returns the updated legacy organization object. Otherwise, it returns the updated registry organization object. * @param {string|null} [requestingUserUUID=null] - The user UUID representing the requester, used for audit documentation. If null, no audit document is created. * * @returns {Promise} A promise that resolves to a plain JavaScript object representing the updated organization, stripped of internal properties and empty values. */ - async updateOrg (shortName, incomingParameters, options = {}, isLegacyObject = false, requestingUserUUID = null) { + async updateOrg (shortName, incomingParameters, options = {}, isLegacyObject = false, requestingUserUUID = null, isAdmin = false, isSecretariat = false) { const { deepRemoveEmpty } = require('../utils/utils') const OrgRepository = require('./orgRepository') // If we get here, we know the org exists const legacyOrgRepo = new OrgRepository() + const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options) - const registryOrg = await this.findOneByShortName(shortName, options) + let registryOrg = await this.findOneByShortName(shortName, options) // Both legacy and registry - if (shortName && typeof shortName === 'string' && shortName.trim() !== '') { - registryOrg.short_name = incomingParameters?.new_short_name ?? registryOrg.short_name - legacyOrg.short_name = incomingParameters?.new_short_name ?? legacyOrg.short_name + if (incomingParameters?.new_short_name) { + registryOrg.short_name = incomingParameters.new_short_name + legacyOrg.short_name = incomingParameters.new_short_name } registryOrg.long_name = incomingParameters?.name ?? registryOrg.long_name legacyOrg.name = incomingParameters?.name ?? legacyOrg.name // TODO: We should probably limit this so it only puts in things that we allow - // Deal with the special way roles are added / removed - // TODO: We are going to need to really check this, this works for single adds / removes. But Matt has some good tests that we should run. - // TODO: What should we do if something is a CNA type, and then gets removed. Does its descriminator need to change? const rolesToAdd = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.add'))) const rolesToRemove = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.remove'))) const initialRoles = legacyOrg.authority?.active_roles ?? [] const finalRoles = [...new Set([...initialRoles, ...rolesToAdd])].filter(role => !rolesToRemove.includes(role)) + + let roleChange = false + // Check if final roles match the original roles in the registry org + if (!_.isEqual(finalRoles.sort(), registryOrg.authority.sort())) { + roleChange = true + } + + // Update authority and discriminator based on role changes registryOrg.authority = finalRoles + // Determine the target model based on the new authority + let TargetModel = null + if (finalRoles.includes('SECRETARIAT')) { + TargetModel = SecretariatOrgModel + } else if (finalRoles.includes('CNA')) { + TargetModel = CNAOrgModel + } else if (finalRoles.includes('ADP')) { + TargetModel = ADPOrgModel + } else if (finalRoles.includes('BULK_DOWNLOAD')) { + TargetModel = BulkDownloadModel + } + + // Save changes - handle possible model type change + if (TargetModel && roleChange) { + const oldId = registryOrg._id + // Remove the old document + await BaseOrgModel.deleteOne({ _id: oldId }, options) + // Create a new document of the correct type, preserving the UUID + const newDocData = registryOrg.toObject() + delete newDocData.__t + newDocData._id = oldId + const newDoc = new TargetModel(newDocData) + // Save the new document (validation will now use the correct schema) + await newDoc.save(options) + // Replace the reference so later code works with the newly saved document + registryOrg = newDoc + } _.set(legacyOrg, 'authority.active_roles', finalRoles) + const directRegistryKeys = [ + 'root_or_tlr', + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'oversees', + 'reports_to', + 'contact_info', // Handles all nested contact_info fields automatically + 'cna_role_type', + 'cna_country', + 'vulnerability_advisory_locations', + 'advisory_location_require_credentials', + 'industry', + 'tl_root_start_date', + 'is_cna_discussion_list' + ] + + // Create a patch object by picking only the defined, relevant keys + // We filter out undefined values so _.merge doesn't overwrite existing fields with undefined + const registryUpdates = _.omitBy( + _.pick(incomingParameters, directRegistryKeys), + _.isUndefined + ) + + // Apply the patch object. + _.merge(registryOrg, registryUpdates) + // Registry Only Stuff // Only a CNA object can have quota - if (registryOrg.__t === 'CNAOrg') { - registryOrg.hard_quota = incomingParameters?.id_quota ?? registryOrg.hard_quota + if (registryOrg.__t === 'CNAOrg' && incomingParameters?.id_quota !== undefined) { + registryOrg.hard_quota = incomingParameters.id_quota } - registryOrg.root_or_tlr = incomingParameters?.root_or_tlr ?? registryOrg.root_or_tlr - registryOrg.charter_or_scope = incomingParameters?.charter_or_scope ?? registryOrg.charter_or_scope - registryOrg.disclosure_policy = incomingParameters?.disclosure_policy ?? registryOrg.disclosure_policy - registryOrg.product_list = incomingParameters?.product_list ?? registryOrg.product_list + const legacyUpdates = {} - registryOrg.oversees = incomingParameters?.oversees ?? registryOrg.oversees - registryOrg.reports_to = incomingParameters?.reports_to ?? registryOrg.reports_to; + // legacy Only Stuff + if (incomingParameters.id_quota !== undefined) { + _.set(legacyUpdates, 'policies.id_quota', incomingParameters.id_quota) + } - ['contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website'].forEach(field => { - _.set(registryOrg, field, _.get(incomingParameters, field, _.get(registryOrg, field, ''))) - }) + _.merge(legacyOrg, legacyUpdates) - // legacy Only Stuff - _.set(legacyOrg, 'policies.id_quota', (incomingParameters?.id_quota ?? legacyOrg.policies.id_quota)) + // ADD AUDIT ENTRY AUTOMATICALLY for the registry object before it gets saved. + if (requestingUserUUID) { + try { + const auditRepo = new AuditRepository() + // Check if an audit document exists, if not we need to create one first and seed it with the existing org data + if (!(await auditRepo.findOneByTargetUUID(registryOrg.UUID, options))) { + const currentRegistryOrg = await this.findOneByShortName(shortName, options) + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + currentRegistryOrg.toObject(), + requestingUserUUID, + options + ) + } + // Get the org state before save for comparison + const beforeUpdateOrg = await this.findOneByShortName(shortName, options) + const beforeUpdateObject = beforeUpdateOrg.toObject() + const afterUpdateObject = registryOrg.toObject() + + // Clean objects for comparison (remove Mongoose metadata) + const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + + // Only add audit entry if there are changes + if (!_.isEqual(cleanBefore, cleanAfter)) { + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + registryOrg.toObject(), + requestingUserUUID, + options + ) + } + } catch (auditError) { + } + } // Save changes - await registryOrg.save({ options }) await legacyOrg.save({ options }) + await registryOrg.save({ options }) if (isLegacyObject) { const plainJavascriptLegacyOrg = legacyOrg.toObject() delete plainJavascriptLegacyOrg.__v @@ -382,21 +511,6 @@ class BaseOrgRepository extends BaseRepository { return deepRemoveEmpty(plainJavascriptLegacyOrg) } - // ADD AUDIT ENTRY AUTOMATICALLY for the registry object. At this point permissions and object validation have been done. - if (requestingUserUUID) { - try { - const auditRepo = new AuditRepository() - await auditRepo.appendToAuditHistory( - registryOrg.UUID, - registryOrg.toObject(), - requestingUserUUID, - options - ) - } catch (auditError) { - // Don't fail the transaction if audit fails - just log it - } - } - const plainJavascriptRegistryOrg = registryOrg.toObject() // Remove private things delete plainJavascriptRegistryOrg.__v @@ -405,6 +519,25 @@ class BaseOrgRepository extends BaseRepository { return deepRemoveEmpty(plainJavascriptRegistryOrg) } + getJointApprovalFields (orgObjectOriginal, orgObjectUpdated, isLegacyObject = false) { + // Get the list of fields that require joint approval + let jointApprovalFields + if (isLegacyObject) { + jointApprovalFields = getConstants().JOINT_APPROVAL_FIELDS_LEGACY + } else { + jointApprovalFields = getConstants().JOINT_APPROVAL_FIELDS + } + + // Filter the list to find only fields that have changed + const changedFields = _.filter(jointApprovalFields, field => { + // Check if the value in the original object is different from the updated object + return _.get(orgObjectOriginal, field) !== _.get(orgObjectUpdated, field) + }) + + // Return the array of fields that had changes (will be empty if none changed) + return changedFields + } + /** * @async * @function updateOrgFull @@ -418,56 +551,151 @@ class BaseOrgRepository extends BaseRepository { * * @returns {Promise} A promise that resolves to a plain JavaScript object representing the updated organization, stripped of internal properties and empty values. */ - async updateOrgFull (shortName, incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null) { + async updateOrgFull (shortName, incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null, isAdmin = false, isSecretariat = false) { + // TODO: Fix these imports, remove the circular imports const { deepRemoveEmpty } = require('../utils/utils') const OrgRepository = require('./orgRepository') + const ReviewObjectRepository = require('./reviewObjectRepository') + const BaseUserRepository = require('./baseUserRepository') + const legacyOrgRepo = new OrgRepository() + const reviewObjectRepo = new ReviewObjectRepository() + const userRepo = new BaseUserRepository() + const conversationRepo = new ConversationRepository() const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options) const registryOrg = await this.findOneByShortName(shortName, options) + // check to see if there is a review object: + const reviewObject = await reviewObjectRepo.getOrgReviewObjectByOrgUUID(registryOrg.UUID) + const { conversation, ...incomingOrgBody } = incomingOrg let legacyObjectRaw let registryObjectRaw if (isLegacyObject) { - legacyObjectRaw = incomingOrg - registryObjectRaw = this.convertLegacyToRegistry(incomingOrg) + legacyObjectRaw = incomingOrgBody + registryObjectRaw = this.convertLegacyToRegistry(incomingOrgBody) } else { - registryObjectRaw = incomingOrg - legacyObjectRaw = this.convertRegistryToLegacy(incomingOrg) + registryObjectRaw = incomingOrgBody + legacyObjectRaw = this.convertRegistryToLegacy(incomingOrgBody) + } + + // Checking for joint approval fields + const jointApprovalFieldsRegistry = this.getJointApprovalFields(registryOrg, registryObjectRaw) + const jointApprovalFieldsLegacy = this.getJointApprovalFields(legacyOrg, legacyObjectRaw, true) + let updatedRegistryOrg = null + let updatedLegacyOrg = null + let jointApprovalRegistry = null + // If there are no joint approval fields, merge the original and updated objects. Otherwise, update the registry object and legacy object separately considering joint approval. + if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { + updatedLegacyOrg = _.merge(legacyOrg, legacyObjectRaw) + updatedRegistryOrg = _.merge(registryOrg, registryObjectRaw) + } else { + // write the joint approval to the database + jointApprovalRegistry = _.merge({}, registryOrg.toObject(), registryObjectRaw) + let updatedReviewObj + if (reviewObject) { + updatedReviewObj = await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, { options }) + } else { + updatedReviewObj = await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, { options }) + } + // handle conversation + const requestingUser = await userRepo.findUserByUUID(requestingUserUUID) + if (conversation && conversation.length) { + await conversationRepo.processConversationHistory(conversation, updatedReviewObj.uuid, requestingUser, isSecretariat, { options }) + } + updatedRegistryOrg = _.merge(registryOrg, _.omit(registryObjectRaw, jointApprovalFieldsRegistry)) + updatedLegacyOrg = _.merge(legacyOrg, _.omit(legacyObjectRaw, jointApprovalFieldsLegacy)) + } + + // ADD AUDIT ENTRY AUTOMATICALLY for the registry object before it gets saved. + if (requestingUserUUID) { + try { + const auditRepo = new AuditRepository() + // Check if an audit document exists, if not we need to create one first and seed it with the existing org data + if (!(await auditRepo.findOneByTargetUUID(registryOrg.UUID, options))) { + const currentRegistryOrg = await this.findOneByShortName(shortName, options) + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + currentRegistryOrg.toObject(), + requestingUserUUID, + options + ) + } + // Get the org state before save for comparison + const beforeUpdateOrg = await this.findOneByShortName(shortName, options) + const beforeUpdateObject = beforeUpdateOrg.toObject() + const afterUpdateObject = registryOrg.toObject() + + // Clean objects for comparison (remove Mongoose metadata) + const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + + // Only add audit entry if there are changes + if (!_.isEqual(cleanBefore, cleanAfter)) { + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + registryOrg.toObject(), + requestingUserUUID, + options + ) + } + } catch (auditError) { + } } - const updatedLegacyOrg = _.merge(legacyOrg, legacyObjectRaw) - const updatedRegistryOrg = _.merge(registryOrg, registryObjectRaw) + // Handle possible authority (discriminator) changes that require a different Mongoose model + let roleChange = false + if (!_.isEqual([...registryOrg?.authority].sort(), [...updatedRegistryOrg?.authority].sort())) { + roleChange = true + } + + // Determine the correct model based on the updated authority + let TargetModel = null + if (updatedRegistryOrg.authority?.includes('SECRETARIAT')) { + TargetModel = SecretariatOrgModel + } else if (updatedRegistryOrg.authority?.includes('CNA')) { + TargetModel = CNAOrgModel + } else if (updatedRegistryOrg.authority?.includes('ADP')) { + TargetModel = ADPOrgModel + } else if (updatedRegistryOrg.authority?.includes('BULK_DOWNLOAD')) { + TargetModel = BulkDownloadModel + } + + // If the model type has changed, replace the document with a new one of the correct type + if (TargetModel && roleChange) { + const oldId = updatedRegistryOrg._id + // Remove the old document + await BaseOrgModel.deleteOne({ _id: oldId }, options) + // Prepare data for the new document, preserving the UUID and _id + const newDocData = updatedRegistryOrg.toObject() + delete newDocData.__t + newDocData._id = oldId + const newDoc = new TargetModel(newDocData) + await newDoc.save(options) + // Update reference so subsequent code works with the newly saved document + updatedRegistryOrg = newDoc + } + + try { + await updatedLegacyOrg.save(options) + await updatedRegistryOrg.save(options) + } catch (error) { + throw new Error(`Failed to update organization ${shortName}. Error: ${error.message}`) + } - // Save changes - await updatedLegacyOrg.save({ options }) - await updatedRegistryOrg.save({ options }) if (isLegacyObject) { const plainJavascriptLegacyOrg = updatedLegacyOrg.toObject() delete plainJavascriptLegacyOrg.__v delete plainJavascriptLegacyOrg._id + plainJavascriptLegacyOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) return deepRemoveEmpty(plainJavascriptLegacyOrg) } - // ADD AUDIT ENTRY AUTOMATICALLY for the registry object. At this point permissions and object validation have been done. - if (requestingUserUUID) { - try { - const auditRepo = new AuditRepository() - await auditRepo.appendToAuditHistory( - updatedRegistryOrg.UUID, - updatedRegistryOrg.toObject(), - requestingUserUUID, - options - ) - } catch (auditError) { - // Don't fail the transaction if audit fails - just log it - } - } - const plainJavascriptRegistryOrg = updatedRegistryOrg.toObject() // Remove private things delete plainJavascriptRegistryOrg.__v delete plainJavascriptRegistryOrg._id delete plainJavascriptRegistryOrg.__t + plainJavascriptRegistryOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) return deepRemoveEmpty(plainJavascriptRegistryOrg) } @@ -493,14 +721,22 @@ class BaseOrgRepository extends BaseRepository { if (Array.isArray(org.authority)) { // User passed in an array, we need to decide how we handle this. if (org.authority.includes('SECRETARIAT')) { - org.authority = 'SECRETARIAT' + org.authority = ['SECRETARIAT'] validateObject = SecretariatOrgModel.validateOrg(org) } else { // We are not a secretariat, so we need to take most priv if (org.authority.includes('CNA') || org.authority.length === 0) { - org.authority = 'CNA' + org.authority = ['CNA'] validateObject = CNAOrgModel.validateOrg(org) } + if (org.authority.includes('ADP')) { + org.authority = ['ADP'] + validateObject = ADPOrgModel.validateOrg(org) + } + if (org.authority.includes('BULK_DOWNLOAD')) { + org.authority = ['BULK_DOWNLOAD'] + validateObject = BulkDownloadModel.validateOrg(org) + } } } else { if (org.authority === 'ADP') { diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index 85b036fce..46ee0cd2c 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -33,6 +33,12 @@ function setAggregateRegistryUserObj (query) { return [ { $match: query + }, + { + $project: { + _id: false, + secret: false + } } ] } @@ -235,6 +241,7 @@ class BaseUserRepository extends BaseRepository { delete rawRegistryUserJson._id delete rawRegistryUserJson.__v delete rawRegistryUserJson.authority + delete rawRegistryUserJson.role return deepRemoveEmpty(rawRegistryUserJson) } @@ -316,6 +323,60 @@ class BaseUserRepository extends BaseRepository { return deepRemoveEmpty(plainJavascriptRegistryUser) } + async updateUserFull (identifier, incomingUser, options = {}, isLegacyObject = false) { + const legacyUserRepo = new UserRepository() + + // Find registry user by UUID + const registryUser = await this.findUserByUUID(identifier, options) + if (!registryUser) { + throw new Error('Registry user not found') + } + + // Find legacy user + const legacyUser = await legacyUserRepo.findOneByUUID(identifier) + if (!legacyUser) { + throw new Error('Legacy user not found') + } + + let legacyObjectRaw + let registryObjectRaw + + if (isLegacyObject) { + legacyObjectRaw = incomingUser + registryObjectRaw = this.convertRegistryToLegacy(incomingUser) + } else { + registryObjectRaw = incomingUser + legacyObjectRaw = this.convertRegistryToLegacy(incomingUser) + } + + const updatedLegacyUser = _.merge(legacyUser, legacyObjectRaw) + const updatedRegistryUser = _.merge(registryUser, registryObjectRaw) + + try { + await updatedLegacyUser.save({ options }) + await updatedRegistryUser.save({ options }) + } catch (error) { + throw new Error('Failed to update user') + } + + if (isLegacyObject) { + const plain = updatedLegacyUser.toObject() + delete plain._id + delete plain.__v + delete plain.secret + return plain + } + + // Retrieve updated registry user + const plainJsRegistryUser = updatedRegistryUser.toObject() + delete plainJsRegistryUser._id + delete plainJsRegistryUser.__v + delete plainJsRegistryUser.secret + delete plainJsRegistryUser.authority + + return plainJsRegistryUser + } + async resetSecret (username, orgShortName, options = {}, isLegacyObject = false) { const legacyUserRepo = new UserRepository() const baseOrgRepository = new BaseOrgRepository() @@ -389,8 +450,6 @@ class BaseUserRepository extends BaseRepository { async getAllUsersByOrgShortname (orgShortname, options = {}, returnLegacyFormat = false) { const CONSTANTS = getConstants() const baseOrgRepository = new BaseOrgRepository() - console.log('Repository is using model:', BaseOrgModel.modelName) - console.log('Model is targeting collection:', BaseOrgModel.collection.name) const userRepository = new UserRepository() const org = await baseOrgRepository.findOneByShortName(orgShortname) const usersInOrg = org.toObject().users diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index 1844718c1..d7e405ff0 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -36,24 +36,8 @@ class ConversationRepository extends BaseRepository { } async getAllByTargetUUID (targetUUID, options = {}) { - const agt = [ - { - $match: { - target_uuid: targetUUID - } - } - ] - const pg = await this.aggregatePaginate(agt, options) - const data = { conversations: pg.itemsList } - if (pg.itemCount >= options.limit) { - data.totalCount = pg.itemCount - data.itemsPerPage = pg.itemsPerPage - data.pageCount = pg.pageCount - data.currentPage = pg.currentPage - data.prevPage = pg.prevPage - data.nextPage = pg.nextPage - } - return data + const conversations = await ConversationModel.find({ target_uuid: targetUUID }, null, options) + return conversations.map(convo => convo.toObject()) } async createConversation (body, options = {}) { @@ -72,6 +56,30 @@ class ConversationRepository extends BaseRepository { const result = await conversation.save(options) return result.toObject() } + + // Takes in a list of conversations representing the conversation history for + // an org and creates/updates the objects as necessary + async processConversationHistory (conversationList, targetUUID, user, isSecretariat, options = {}) { + const promises = conversationList.map(convo => { + return (async () => { + const populatedConvo = { + UUID: convo.UUID || undefined, + author_id: convo.author_id || user.UUID, + author_name: convo.author_name || (isSecretariat ? 'Secretariat' : [user.name?.first, user.name?.last].join(' ')), + author_role: convo.author_role || (isSecretariat ? 'Secretariat' : 'Partner'), + visibility: !isSecretariat ? 'public' : (convo.visibility || 'private'), + body: convo.body + } + if (populatedConvo.UUID) return await this.updateConversation(populatedConvo, populatedConvo.UUID, options) + const newConvo = { + ...populatedConvo, + target_uuid: targetUUID + } + return await this.createConversation(newConvo, options) + })() + }) + return await Promise.all(promises) + } } module.exports = ConversationRepository diff --git a/src/repositories/registryOrgRepository.js b/src/repositories/registryOrgRepository.js deleted file mode 100644 index 3badf2d16..000000000 --- a/src/repositories/registryOrgRepository.js +++ /dev/null @@ -1,104 +0,0 @@ -const BaseRepository = require('./baseRepository') -const RegistryOrg = require('../model/registry-org') -const utils = require('../utils/utils') - -class RegistryOrgRepository extends BaseRepository { - constructor () { - super(RegistryOrg) - } - - async findOneByShortName (shortName, options = {}) { - const query = { short_name: shortName } - // We are returning the whole object here, so no projection is needed - return this.collection.findOne(query, null, options) - } - - async findOneByUUID (UUID) { - return this.collection.findOne().byUUID(UUID) - } - - async getOrgUUID (shortName, options = {}) { - return utils.getOrgUUID(shortName, true, options) // use registryOrgRepository to find org UUID - } - - async getAllOrgs () { - return this.collection.find() - } - - async isSecretariat (shortName, options = {}) { - return utils.isSecretariat(shortName, true, options) - } - - async updateByUUID (uuid, org, options = {}) { - // The filter to find the document - const filter = { UUID: uuid } - const updatePayload = { $set: org } - return this.collection.findOneAndUpdate(filter, updatePayload, options) - } - - async deleteByUUID (uuid) { - return this.collection.deleteOne({ UUID: uuid }) - } - - async removeUserFromOrgList (registryOrgUUID, userUUIDToRemove, isAdmin = false, options = {}) { - if (!registryOrgUUID || !userUUIDToRemove) { - throw new Error('RegistryOrg UUID and User UUID to remove are required for removeUserFromOrgList.') - } - - const filter = { UUID: registryOrgUUID } - const updateOperation = { - $pull: { - users: userUUIDToRemove - } - } - - if (isAdmin) { - updateOperation.$pull['contact_info.admins'] = userUUIDToRemove - } - - try { - const result = await this.collection.updateOne(filter, updateOperation, options) - if (result.matchedCount === 0) { - console.warn(`removeUserFromOrgList: No RegistryOrg found with UUID '${registryOrgUUID}'. User UUID not removed.`) - } else if (result.modifiedCount === 0) { - console.info(`removeUserFromOrgList: User UUID '${userUUIDToRemove}' was not found in relevant lists for RegistryOrg '${registryOrgUUID}', or no change was needed.`) - } - return result - } catch (error) { - console.error(`Error in removeUserFromOrgList for RegistryOrg ${registryOrgUUID}, User ${userUUIDToRemove}:`, error) - throw error - } - } - - async addUserToOrgList (registryOrgUUID, userUUIDToAdd, isAdmin = false, options = {}) { - if (!registryOrgUUID || !userUUIDToAdd) { - throw new Error('RegistryOrg UUID and User UUID to add are required for addUserToOrgList.') - } - - const filter = { UUID: registryOrgUUID } - const updateOperation = { - $addToSet: { - users: userUUIDToAdd - } - } - - if (isAdmin) { - updateOperation.$addToSet['contact_info.admins'] = userUUIDToAdd - } - - try { - const result = await this.collection.updateOne(filter, updateOperation, options) - if (result.matchedCount === 0) { - console.warn(`addUserToOrgList: No RegistryOrg found with UUID '${registryOrgUUID}'. User UUID not added.`) - } else if (result.modifiedCount === 0 && result.matchedCount === 1) { - console.info(`addUserToOrgList: User UUID '${userUUIDToAdd}' was already present in relevant lists for RegistryOrg '${registryOrgUUID}', or no change was needed.`) - } - return result - } catch (error) { - console.error(`Error in addUserToOrgList for RegistryOrg ${registryOrgUUID}, User ${userUUIDToAdd}:`, error) - throw error - } - } -} - -module.exports = RegistryOrgRepository diff --git a/src/repositories/registryUserRepository.js b/src/repositories/registryUserRepository.js deleted file mode 100644 index da5f4b4b4..000000000 --- a/src/repositories/registryUserRepository.js +++ /dev/null @@ -1,83 +0,0 @@ -const BaseRepository = require('./baseRepository') -const RegistryUser = require('../model/registry-user') -const utils = require('../utils/utils') - -class RegistryUserRepository extends BaseRepository { - constructor () { - super(RegistryUser) - } - - async getUserUUID (username, orgUUID, options = {}) { - return utils.getUserUUID(username, orgUUID, true, options) - } - - async findOneByUUID (UUID) { - return this.collection.findOne().byUUID(UUID) - } - - async findUsersByOrgUUID (orgUUID, options = {}) { - const filter = { 'org_affiliations.org_id': orgUUID } - return this.collection.countDocuments(filter, options) - } - - async isSecretariat (org, options = {}) { - return utils.isSecretariat(org, true, options) - } - - async isAdmin (username, orgShortname, options = {}) { - return utils.isAdmin(username, orgShortname, true, options) - } - - async isAdminUUID (username, OrgUUID, options = {}) { - return utils.isAdminUUID(username, OrgUUID, true, options) - } - - async updateByUserNameAndOrgUUID (username, orgUUID, user, options = {}) { - const filter = { user_id: username, 'org_affiliations.org_id': orgUUID } - const updatePayload = { $set: user } - return this.collection.findOneAndUpdate(filter, updatePayload, options) - } - - async updateByUUID (uuid, updatePayload, options = {}) { - const filter = { UUID: uuid } - - const updateOperation = { $set: updatePayload } - - return this.collection.findOneAndUpdate(filter, updateOperation, options) - } - - async findOneByUserNameAndOrgUUID (userName, orgUUID, projection = null, options = {}) { - const query = { user_id: userName, 'org_affiliations.org_id': orgUUID } - return this.collection.findOne(query, projection, options) - } - - async deleteByUUID (uuid) { - return this.collection.deleteOne({ UUID: uuid }) - } - - async addOrgToUserAffiliation (userUUID, orgUUID, options = {}) { - const filter = { UUID: userUUID } - const updateOperation = { - $addToSet: { - org_affiliations: [{ - org_id: orgUUID - }] - } - } - - try { - const result = await this.collection.updateOne(filter, updateOperation, options) - if (result.matchedCount === 0) { - console.warn(`addOrgToUserAffiliation: No ORG found with UUID '${orgUUID}'. User UUID not added.`) - } else if (result.modifiedCount === 0 && result.matchedCount === 1) { - console.info(`addOrgToUserAffiliation: ORG UUID '${orgUUID}' was already present in relevant lists for RegistryUser '${userUUID}', or no change was needed.`) - } - return result - } catch (error) { - console.error(`Error in addOrgToUserAffiliation for RegistryOrg ${orgUUID}, User ${userUUID}:`, error) - throw error - } - } -} - -module.exports = RegistryUserRepository diff --git a/src/repositories/repositoryFactory.js b/src/repositories/repositoryFactory.js index 41b16d1bc..4750fffea 100644 --- a/src/repositories/repositoryFactory.js +++ b/src/repositories/repositoryFactory.js @@ -3,8 +3,6 @@ const CveRepository = require('./cveRepository') const CveIdRepository = require('./cveIdRepository') const CveIdRangeRepository = require('./cveIdRangeRepository') const UserRepository = require('./userRepository') -const RegistryUserRepository = require('./registryUserRepository') -const RegistryOrgRepository = require('./registryOrgRepository') const BaseOrgRepository = require('./baseOrgRepository') const BaseUserRepository = require('./baseUserRepository') const ConversationRepository = require('./conversationRepository') @@ -36,16 +34,6 @@ class RepositoryFactory { return repo } - getRegistryUserRepository () { - const repo = new RegistryUserRepository() - return repo - } - - getRegistryOrgRepository () { - const repo = new RegistryOrgRepository() - return repo - } - getBaseOrgRepository () { const repo = new BaseOrgRepository() return repo diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index b96fff4b4..1a3c24ef5 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -20,6 +20,20 @@ class ReviewObjectRepository extends BaseRepository { return reviewObject || null } + async findOneByUUIDWithConversation (UUID, isSecretariat, options = {}) { + const ConversationRepository = require('./conversationRepository') + const conversationRepository = new ConversationRepository() + let reviewObject + const reviewObjectRaw = await ReviewObjectModel.findOne({ uuid: UUID }, null, options) + if (reviewObjectRaw) { + reviewObject = reviewObjectRaw.toObject() + const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid) + if (conversations && conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') + } + + return reviewObject || null + } + async getAllReviewObjects (options = {}) { const reviewObjects = await ReviewObjectModel.find({}, null, options) return reviewObjects || [] @@ -30,34 +44,62 @@ class ReviewObjectRepository extends BaseRepository { return result.deletedCount } - async getOrgReviewObjectByOrgShortname (orgShortName, options = {}) { + async getOrgReviewObjectStandaloneByRequestedOrgShortname (requestedOrgShortName, options = {}) { + const reviewObject = await ReviewObjectModel.findOne({ 'new_review_data.short_name': requestedOrgShortName }, null, options) + + return reviewObject || null + } + + async getOrgReviewObjectByOrgShortname (orgShortName, isSecretariat, options = {}) { const baseOrgRepository = new BaseOrgRepository() + const ConversationRepository = require('./conversationRepository') + const conversationRepository = new ConversationRepository() const org = await baseOrgRepository.findOneByShortName(orgShortName) if (!org) { return null } - const reviewObject = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) + let reviewObject + const reviewObjectRaw = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) + if (reviewObjectRaw) { + reviewObject = reviewObjectRaw.toObject() + const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid) + if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') + } return reviewObject || null } - async getOrgReviewObjectByOrgUUID (orgUUID, options = {}) { + async getOrgReviewObjectByOrgUUID (orgUUID, isSecretariat, options = {}) { const baseOrgRepository = new BaseOrgRepository() + const ConversationRepository = require('./conversationRepository') + const conversationRepository = new ConversationRepository() const org = await baseOrgRepository.findOneByUUID(orgUUID) if (!org) { return null } - const reviewObject = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) + let reviewObject + const reviewObjectRaw = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) + if (reviewObjectRaw) { + reviewObject = reviewObjectRaw.toObject() + const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid) + if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') + } return reviewObject || null } - async createReviewOrgObject (body, options = {}) { - console.log('Creating review object for organization:', body.target_object_uuid) - body.uuid = uuid.v4() - const reviewObject = new ReviewObjectModel(body) - const result = await reviewObject.save(options) - return result.toObject() + async createReviewOrgObject (orgBody, options = {}) { + console.log('Creating review object for organization:', orgBody.UUID) + const reviewObjectRaw = { + uuid: uuid.v4(), + target_object_uuid: orgBody.UUID, + status: 'pending', + new_review_data: orgBody || {} + } + + const reviewObject = new ReviewObjectModel(reviewObjectRaw) + await reviewObject.save({ options }) + return reviewObject.toObject() } async updateReviewOrgObject (body, UUID, options = {}) { @@ -67,12 +109,34 @@ class ReviewObjectRepository extends BaseRepository { return null } - // For each item waiting for approval, for testing we are going to just do shortname - reviewObject.new_review_data.short_name = body.new_review_data.short_name || reviewObject.new_review_data.short_name + reviewObject.new_review_data = body - const result = await reviewObject.save(options) + const result = await reviewObject.save({ options }) return result.toObject() } + + async approveReviewOrgObject (UUID, requestingUserUUID, options = {}) { + console.log('Approving review object with UUID:', UUID) + const reviewObject = await this.findOneByUUID(UUID, options) + if (!reviewObject) { + return null + } + + const baseOrgRepository = new BaseOrgRepository() + const org = await baseOrgRepository.findOneByUUID(reviewObject.target_object_uuid) + if (!org) { + return null + } + + // We need to trigger the org to update + await baseOrgRepository.updateOrgFull(org.short_name, reviewObject.new_review_data, options, false, requestingUserUUID, false, true) + + reviewObject.status = 'approved' + + await reviewObject.save({ options }) + const result = reviewObject.toObject() + return result + } } module.exports = ReviewObjectRepository diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 69bbb4bf2..efd98aa9d 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -18,6 +18,8 @@ const Org = require('../model/org') const User = require('../model/user') const BaseOrg = require('../model/baseorg') const BaseUser = require('../model/baseuser') +const ReviewObject = require('../model/reviewobject') +const Conversation = require('../model/conversation') const error = new errors.IDRError() @@ -28,7 +30,9 @@ const populateTheseCollections = { User: User, Org: Org, BaseOrg: BaseOrg, - BaseUser: BaseUser + BaseUser: BaseUser, + ReviewObject: ReviewObject, + Conversation: Conversation } const indexesToCreate = { diff --git a/test-http/src/test/org_user_tests/org.py b/test-http/src/test/org_user_tests/org.py index f2ca448b9..a9d324b4b 100644 --- a/test-http/src/test/org_user_tests/org.py +++ b/test-http/src/test/org_user_tests/org.py @@ -54,7 +54,7 @@ def test_get_all_orgs(): """secretariat users can request a list of all organizations""" res = requests.get(f"{env.AWG_BASE_URL}{ORG_URL}", headers=utils.BASE_HEADERS) - ok_response_contains(res, '"active_roles":["SECRETARIAT","CNA"]') + ok_response_contains(res, '"active_roles":["CNA"]') assert len(json.loads(res.content.decode())["organizations"]) >= 1 diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js new file mode 100644 index 000000000..6d7a8bc87 --- /dev/null +++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js @@ -0,0 +1,215 @@ +const chai = require('chai') +chai.use(require('chai-http')) +const { expect } = chai +const { v4: uuidv4 } = require('uuid') +const AuditRepo = require('../../../src/repositories/auditRepository') + +const app = require('../../../src/index.js') +const constants = require('../constants.js') + +const secretariatHeaders = { ...constants.headers } +const MAX_SHORTNAME_LENGTH = 32 + +async function createTestOrg (customProps = {}) { + const shortName = uuidv4().slice(0, MAX_SHORTNAME_LENGTH) + const defaultProps = { + short_name: shortName, + long_name: `Test Org ${shortName}`, + hard_quota: 1000, + authority: ['CNA'] + } + + const orgData = { ...defaultProps, ...customProps } + + const res = await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send(orgData) + + expect(res).to.have.status(200) + + return { + shortName: orgData.short_name, + longName: orgData.long_name, + uuid: res.body.created.UUID, + fullResponse: res.body + } +} + +describe('Create and Update Audit Collection with Org Endpoints', () => { + it('Should automatically create audit document when org is created', async () => { + // Create org + const org = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + + // Verify audit was created + const auditRes = await chai.request(app) + .get(`/api/audit/org/${org.uuid}`) + .set(constants.headers) + + expect(auditRes).to.have.status(200) + + // Verify audit structure + const audit = auditRes.body + expect(audit).to.have.property('uuid') + expect(audit).to.have.property('target_uuid') + expect(audit).to.have.property('history') + expect(audit.target_uuid).to.equal(org.uuid) + expect(audit.history).to.be.an('array').with.lengthOf(1) + + // Verify initial history entry + const initialEntry = audit.history[0] + expect(initialEntry).to.have.property('audit_object') + expect(initialEntry.timestamp).to.be.a('string') + expect(initialEntry.change_author).to.be.a('string') + + // Verify audit object matches created org + const auditObject = initialEntry.audit_object + expect(auditObject.short_name).to.equal(org.shortName) + expect(auditObject.long_name).to.equal(org.longName) + expect(auditObject.hard_quota).to.equal(1500) + expect(auditObject.UUID).to.equal(org.uuid) + }) + + it('Should create separate audit documents for multiple orgs', async () => { + // Create multiple orgs + const [org1, org2, org3] = await Promise.all([ + createTestOrg({ long_name: 'First Org' }), + createTestOrg({ long_name: 'Second Org' }), + createTestOrg({ long_name: 'Third Org' }) + ]) + + // Verify each has its own audit + const audits = await Promise.all([ + chai.request(app).get(`/api/audit/org/${org1.uuid}`).set(constants.headers), + chai.request(app).get(`/api/audit/org/${org2.uuid}`).set(constants.headers), + chai.request(app).get(`/api/audit/org/${org3.uuid}`).set(constants.headers) + ]) + + // Each should have its own audit document + audits.forEach((auditRes, index) => { + expect(auditRes).to.have.status(200) + const org = [org1, org2, org3][index] + expect(auditRes.body.target_uuid).to.equal(org.uuid) + expect(auditRes.body.history[0].audit_object.long_name).to.equal(org.longName) + }) + + // Audit UUIDs should all be different + const auditUUIDs = audits.map(res => res.body.uuid) + expect(new Set(auditUUIDs).size).to.equal(3) + }) + + it('Should NOT add audit entry when updating with no actual changes', async () => { + const org = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + + // Now update with same values + const updateResAgain = await chai.request(app) + .put(`/api/registry/org/${org.shortName}?long_name=${org.longName}`) + .set(secretariatHeaders) + expect(updateResAgain).to.have.status(200) + + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${org.uuid}`) + .set(constants.headers) + + expect(auditRes.body.history).to.have.lengthOf(1) + }) + + it('Should add audit entry when single field is changed', async () => { + const testOrg = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + + // Update org name + const updateRes = await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=100`) + .set(secretariatHeaders) + + expect(updateRes).to.have.status(200) + + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${testOrg.shortName}`) + .set(constants.headers) + + expect(auditRes.body.history).to.have.lengthOf(2) + + // Original entry + expect(auditRes.body.history[0].audit_object.hard_quota).to.equal(1500) + + // New entry + expect(auditRes.body.history[1].audit_object.hard_quota).to.equal(100) + }) + + it('Should maintain chronological order in audit history', async () => { + const testOrg = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + // Make sequential updates + const updatedRes1 = await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=2000`) + .set(secretariatHeaders) + expect(updatedRes1).to.have.status(200) + + const updatedRes2 = await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=3000`) + .set(secretariatHeaders) + expect(updatedRes2).to.have.status(200) + + const updatedRes3 = await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=4000`) + .set(secretariatHeaders) + expect(updatedRes3).to.have.status(200) + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${testOrg.uuid}`) + .set(constants.headers) + + expect(auditRes.body.history).to.have.lengthOf(4) + + // Verify chronological order + const quotas = auditRes.body.history.map(h => h.audit_object.hard_quota) + expect(quotas).to.deep.equal([1500, 2000, 3000, 4000]) + + // Verify timestamps are in order + for (let i = 1; i < auditRes.body.history.length; i++) { + const prev = new Date(auditRes.body.history[i - 1].timestamp) + const curr = new Date(auditRes.body.history[i].timestamp) + expect(curr.getTime()).to.be.greaterThan(prev.getTime()) + } + }) + + it('Should create an audit when updating an Org if it does not exist', async () => { + const testOrg = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + // Manually delete audit document + const repo = new AuditRepo() + await repo.deleteByTargetUUID(testOrg.uuid) + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${testOrg.uuid}`) + .set(constants.headers) + expect(auditRes).to.have.status(404) + // Now update org to trigger audit creation + const updateRes = await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=2500`) + .set(secretariatHeaders) + expect(updateRes).to.have.status(200) + // Check audit history + const auditResCreation = await chai.request(app) + .get(`/api/audit/org/${testOrg.uuid}`) + .set(constants.headers) + // Should have 2 entries: initial creation of current org object + new update + expect(auditResCreation.body.history).to.have.lengthOf(2) + }) +}) diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index c27d7d971..de4946825 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -373,7 +373,7 @@ const testRegistryOrg = { org_email: 'contact@test.org', website: 'https://test.org' }, - authority: 'CNA', + authority: ['CNA'], hard_quota: 100000 } @@ -387,7 +387,7 @@ const testRegistryOrg2 = { org_email: 'contact@test.org', website: 'https://test.org' }, - authority: 'CNA', + authority: ['CNA'], hard_quota: 100000 } @@ -415,7 +415,7 @@ const existingRegistryOrg = { org_email: 'contact@test.org', website: 'https://test.org' }, - authority: 'CNA', + authority: ['CNA'], hard_quota: 100000 } diff --git a/test/integration-tests/conversation/conversationTest.js b/test/integration-tests/conversation/conversationTest.js new file mode 100644 index 000000000..108c88960 --- /dev/null +++ b/test/integration-tests/conversation/conversationTest.js @@ -0,0 +1,134 @@ +/* eslint-disable no-unused-expressions */ + +const chai = require('chai') +const expect = chai.expect +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +describe('Testing Conversation endpoints', () => { + let orgUUID + let conversationUUID + + before(async () => { + await chai + .request(app) + .get('/api/org/win_5') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + orgUUID = res.body.UUID + }) + }) + + context('Positive Tests', () => { + it('Should create a conversation', async () => { + const conversation = { + visibility: 'public', + body: 'test' + } + await chai.request(app) + .post(`/api/conversation/target/${orgUUID}`) + .set(constants.headers) + .send(conversation) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('UUID') + conversationUUID = res.body.UUID + + expect(res.body).to.haveOwnProperty('target_uuid') + expect(res.body.target_uuid).to.equal(orgUUID) + + expect(res.body).to.haveOwnProperty('author_id') + expect(res.body).to.haveOwnProperty('author_name') + + expect(res.body).to.haveOwnProperty('author_role') + expect(res.body.author_role).to.equal('Secretariat') + + expect(res.body).to.haveOwnProperty('visibility') + expect(res.body.visibility).to.equal('public') + + expect(res.body).to.haveOwnProperty('body') + expect(res.body.body).to.equal('test') + }) + }) + it('Should get all conversations', async () => { + await chai.request(app) + .get('/api/conversation') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('conversations') + expect(res.body.conversations).to.be.an('array') + expect(res.body.conversations).to.have.lengthOf(1) + }) + }) + it('Should get all conversations for target UUID', async () => { + await chai.request(app) + .get(`/api/conversation/target/${orgUUID}`) + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.be.an('array') + expect(res.body).to.have.lengthOf(1) + expect(res.body[0]).to.haveOwnProperty('target_uuid') + expect(res.body[0].target_uuid).to.equal(orgUUID) + }) + }) + it('Should update the message for a conversation', async () => { + const updateBody = { + body: 'test update' + } + await chai.request(app) + .put(`/api/conversation/${conversationUUID}/message`) + .set(constants.headers) + .send(updateBody) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('UUID') + expect(res.body.UUID).to.equal(conversationUUID) + expect(res.body).to.haveOwnProperty('body') + expect(res.body.body).to.equal('test update') + }) + }) + }) + + context('Negative Tests', () => { + it('Should fail to post a conversation with no body', async () => { + await chai.request(app) + .post(`/api/conversation/target/${orgUUID}`) + .set(constants.headers) + .send({}) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(400) + + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('Missing required field body') + }) + }) + it('Should fail to update a conversation message with no body', async () => { + await chai.request(app) + .put(`/api/conversation/${conversationUUID}/message`) + .set(constants.headers) + .send({}) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(400) + + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('Missing required field body') + }) + }) + }) +}) diff --git a/test/integration-tests/org/postOrgUsersTest.js b/test/integration-tests/org/postOrgUsersTest.js index aa7e0e8c9..098763b1b 100644 --- a/test/integration-tests/org/postOrgUsersTest.js +++ b/test/integration-tests/org/postOrgUsersTest.js @@ -11,7 +11,6 @@ const User = require('../../../src/model/user') // const RegistryUser = require('../../../src/model/registry-user.js') const shortName = { shortname: 'win_5' } -const registryFlag = { registry: true } describe('Testing user post endpoint', () => { let orgUuid @@ -88,11 +87,10 @@ describe('Testing user post endpoint', () => { it('Fails creation of user for bad long first name with registry enabled', async () => { await chai .request(app) - .post('/api/org/win_5/user') + .post('/api/registry/org/win_5/user') .set({ ...constants.headers, ...shortName }) - .query(registryFlag) .send({ - user_id: 'fakeregistryuser9999', + username: 'fakeregistryuser9999', name: { first: 'VerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnm1', @@ -100,9 +98,7 @@ describe('Testing user post endpoint', () => { middle: 'Cool', suffix: 'Mr.' }, - authority: { - active_roles: ['ADMIN'] - } + role: ['ADMIN'] }) .then((res, err) => { expect(res).to.have.status(400) @@ -141,20 +137,18 @@ describe('Testing user post endpoint', () => { it('Fails creation of user for bad long last name with registry enabled', async () => { await chai .request(app) - .post('/api/org/win_5/user') + .post('/api/registry/org/win_5/user') .set({ ...constants.headers, ...shortName }) - .query(registryFlag) .send({ - user_id: 'fakeregistryuser1000', + username: 'fakeregistryuser1000', name: { first: 'FirstName', last: 'VerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnm1', middle: 'Cool', suffix: 'Mr.' }, - authority: { - active_roles: ['ADMIN'] - } + role: ['ADMIN'] + }) .then((res, err) => { expect(res).to.have.status(400) @@ -193,11 +187,10 @@ describe('Testing user post endpoint', () => { it('Fails creation of user for bad long middle name with registry enabled', async () => { await chai .request(app) - .post('/api/org/win_5/user') + .post('/api/registry/org/win_5/user') .set({ ...constants.headers, ...shortName }) - .query(registryFlag) .send({ - user_id: 'fakeregistryuser1001', + username: 'fakeregistryuser1001', name: { first: 'FirstName', last: 'LastName', @@ -205,9 +198,8 @@ describe('Testing user post endpoint', () => { 'VerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnm1', suffix: 'Mr.' }, - authority: { - active_roles: ['ADMIN'] - } + role: ['ADMIN'] + }) .then((res, err) => { expect(res).to.have.status(400) @@ -246,11 +238,10 @@ describe('Testing user post endpoint', () => { it('Fails creation of user for bad long suffix name with registry enabled', async () => { await chai .request(app) - .post('/api/org/win_5/user') + .post('/api/registry/org/win_5/user') .set({ ...constants.headers, ...shortName }) - .query(registryFlag) .send({ - user_id: 'fakeregistryuser1002', + username: 'fakeregistryuser1002', name: { first: 'FirstName', last: 'LastName', @@ -258,9 +249,7 @@ describe('Testing user post endpoint', () => { suffix: 'VerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnm1' }, - authority: { - active_roles: ['ADMIN'] - } + role: 'ADMIN' }) .then((res, err) => { expect(res).to.have.status(400) diff --git a/test/integration-tests/org/regularUsersTestRegistryFlag.js b/test/integration-tests/org/regularUsersTestRegistry.js similarity index 94% rename from test/integration-tests/org/regularUsersTestRegistryFlag.js rename to test/integration-tests/org/regularUsersTestRegistry.js index 9cef52e47..6ffe1986d 100644 --- a/test/integration-tests/org/regularUsersTestRegistryFlag.js +++ b/test/integration-tests/org/regularUsersTestRegistry.js @@ -7,12 +7,12 @@ const constants = require('../constants.js') const app = require('../../../src/index.js') const MAX_SHORTNAME_LENGTH = 32 /** - * Unit Tests for testing regular user permissions for Org and User /api/org endpoints with the `registry=true` flag + * Unit Tests for testing regular user permissions for Org and User /api/registry/org */ -describe('Testing regular user permissions for /api/org/ endpoints with `registry=true`', () => { - // Testing USER PUT Endpoints for regular users with `registry=true` flag - describe('Testing USER PUT endpoint with `registry=true`', () => { +describe('Testing regular user permissions for /api/registry/org/ endpoints with ', () => { + // Testing USER PUT Endpoints for regular users with /api/registry/org + describe('Testing USER PUT endpoint ', () => { /* Positive Tests */ context('Positive Test', () => { it('regular user can update their name', async () => { @@ -232,8 +232,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing USER POST Endpoints for regular users with `registry=true` flag - describe('Testing USER POST endpoint with `registry=true`', () => { + // Testing USER POST Endpoints for regular users with /api/registry/org + describe('Testing USER POST endpoint', () => { /* Negative Tests */ context('Negative Test', () => { it('regular user cannot create another user', async () => { @@ -252,8 +252,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing USER GET Endpoints for regular users with `registry=true` flag - describe('Testing USER GET endpoint with `registry=true`', () => { + // Testing USER GET Endpoints for regular users with /api/registry/org + describe('Testing USER GET endpoint with /api/registry/org', () => { /* Positive Tests */ context('Positive Test', () => { it('regular users can view users of the same organization', async () => { @@ -336,8 +336,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing ORG PUT Endpoints for regular users with `registry=true` flag - describe('Testing ORG PUT endpoint with `registry=true`', () => { + // Testing ORG PUT Endpoints for regular users with /api/registry/org + describe('Testing ORG PUT endpoint with /api/registry/org', () => { /* Negative Tests */ context('Negative Test', () => { it('regular user cannot update an organization', async () => { @@ -354,8 +354,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing ORG POST Endpoints for regular users with `registry=true` flag - describe('Testing ORG POST endpoint with `registry=true`', () => { + // Testing ORG POST Endpoints for regular users with /api/registry/org + describe('Testing ORG POST endpoint with /api/registry/org', () => { context('Negative Test', () => { it('regular users cannot create new org', async () => { await chai.request(app) @@ -370,8 +370,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing ORG GET Endpoints for regular users with `registry=true` flag - describe('Testing ORG GET endpoint with `registry=true`', () => { + // Testing ORG GET Endpoints for regular users + describe('Testing Registry ORG GET', () => { /* Positive Tests */ context('Positive Test', () => { it('regular users can view the organization they belong to', async () => { diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 4238bc238..35e897f5b 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -11,7 +11,7 @@ const secretariatHeaders = { ...constants.headers, 'content-type': 'application/ const testRegistryOrg = { short_name: 'registry_org_test', long_name: 'Registry Org Test', - authority: 'CNA', + authority: ['CNA'], hard_quota: 1000 } let createdOrg diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js new file mode 100644 index 000000000..9e6e66ada --- /dev/null +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -0,0 +1,328 @@ +/* eslint-disable no-unused-expressions */ + +const chai = require('chai') +const expect = chai.expect + +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } + +const nonAdminHeaders = { + 'CVE-API-ORG': 'non_secretariat_org', + 'content-type': 'application/json', + 'CVE-API-USER': 'drocca_admin_user' +} + +const nonAdminHeaders2 = { + 'CVE-API-ORG': 'non_with_comments', + 'content-type': 'application/json', + 'CVE-API-USER': 'drocca_admin_user_comments' +} + +const testRegistryOrgForReview = { + short_name: 'non_secretariat_org', + long_name: 'Non Secretariat Org', + authority: ['CNA'], + hard_quota: 1000 +} + +const testRegistryOrgForReviewWithComments = { + short_name: 'non_with_comments', + long_name: 'Non Secretariat Org', + authority: ['CNA'], + hard_quota: 1000 +} + +const testRegistryOrgAdminUser = { + username: 'drocca_admin_user', + active: 'true', + name: { + first: 'David', + last: 'Rocca', + middle: 'N', + suffix: 'I' + }, + authority: { + active_roles: ['Admin'] + } +} + +const testRegistryOrgAdminUserWithComments = { + username: 'drocca_admin_user_comments', + active: 'true', + name: { + first: 'David', + last: 'Rocca', + middle: 'N', + suffix: 'I' + }, + authority: { + active_roles: ['Admin'] + } +} + +describe('Testing Joint approval', () => { + describe('Admin user attempts to edit a joint approval field', () => { + let secret + let orgUUID + let reviewUUID + it('Create an org to use for testing', async () => { + await chai.request(app) + .post('/api/registryOrg') + .set(secretariatHeaders) + .send(testRegistryOrgForReview) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal(testRegistryOrgForReview.short_name + ' organization was successfully created.') + + expect(res.body).to.haveOwnProperty('created') + + expect(res.body.created).to.haveOwnProperty('UUID') + + expect(res.body.created).to.haveOwnProperty('short_name') + expect(res.body.created.short_name).to.equal(testRegistryOrgForReview.short_name) + + expect(res.body.created).to.haveOwnProperty('long_name') + expect(res.body.created.long_name).to.equal(testRegistryOrgForReview.long_name) + + expect(res.body.created).to.haveOwnProperty('authority') + expect(res.body.created.authority).to.deep.equal(['CNA']) + + expect(res.body.created).to.haveOwnProperty('hard_quota') + expect(res.body.created.hard_quota).to.equal(testRegistryOrgForReview.hard_quota) + }) + }) + it('Create an User', async () => { + await chai.request(app) + .post('/api/registry/org/non_secretariat_org/user') + .set(constants.headers) + .send(testRegistryOrgAdminUser) + .then((res, err) => { + expect(err).to.be.undefined + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(testRegistryOrgAdminUser.username) + expect(res).to.have.status(200) + secret = res.body.created.secret + nonAdminHeaders['CVE-API-KEY'] = secret + }) + }) + it('Attempt to change the short name of the org', async () => { + await chai.request(app) + .put('/api/registryOrg/non_secretariat_org') + .set(nonAdminHeaders) + .send({ ...testRegistryOrgForReview, short_name: 'new_non_secretariat_org', hard_quota: 10000 }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.contain('organization was successfully updated, but joint approval is required for some fields.') + orgUUID = res.body.updated.UUID + }) + }) + it('Check to see if an ORG review was created', async () => { + await chai.request(app) + .get(`/api/review/org/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body).to.have.property('status', 'pending') + expect(res.body.target_object_uuid).to.equal(orgUUID) + expect(res.body.new_review_data.short_name).to.equal('new_non_secretariat_org') + reviewUUID = res.body.uuid + }) + }) + it('Check to see if the org was partially updated', async () => { + await chai.request(app) + .get(`/api/registryOrg/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.short_name).to.equal('non_secretariat_org') + expect(res.body.hard_quota).to.equal(10000) + }) + }) + it('Secretariat can approve the ORG review', async function () { + await chai.request(app) + .put(`/api/review/org/${reviewUUID}/approve`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.status).to.equal('approved') + }) + }) + it('Check to see if the org was fully updated', async () => { + await chai.request(app) + .get(`/api/registryOrg/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.short_name).to.equal('new_non_secretariat_org') + expect(res.body.hard_quota).to.equal(10000) + }) + }) + }) + describe('Admin user attempts to edit a joint approval field, Secretariat leaves comment, admin fixes with a comment, secretariat approves', () => { + let secret + let orgUUID + let reviewUUID + it('Create an org to use for testing', async () => { + await chai.request(app) + .post('/api/registryOrg') + .set(secretariatHeaders) + .send(testRegistryOrgForReviewWithComments) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal(testRegistryOrgForReviewWithComments.short_name + ' organization was successfully created.') + + expect(res.body).to.haveOwnProperty('created') + + expect(res.body.created).to.haveOwnProperty('UUID') + + expect(res.body.created).to.haveOwnProperty('short_name') + expect(res.body.created.short_name).to.equal(testRegistryOrgForReviewWithComments.short_name) + + expect(res.body.created).to.haveOwnProperty('long_name') + expect(res.body.created.long_name).to.equal(testRegistryOrgForReviewWithComments.long_name) + + expect(res.body.created).to.haveOwnProperty('authority') + expect(res.body.created.authority).to.deep.equal(['CNA']) + + expect(res.body.created).to.haveOwnProperty('hard_quota') + expect(res.body.created.hard_quota).to.equal(testRegistryOrgForReviewWithComments.hard_quota) + }) + }) + it('Create an User', async () => { + await chai.request(app) + .post('/api/registry/org/non_with_comments/user') + .set(constants.headers) + .send(testRegistryOrgAdminUserWithComments) + .then((res, err) => { + expect(err).to.be.undefined + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(testRegistryOrgAdminUserWithComments.username) + expect(res).to.have.status(200) + secret = res.body.created.secret + nonAdminHeaders2['CVE-API-KEY'] = secret + }) + }) + it('Attempt to change the short name of the org', async () => { + await chai.request(app) + .put('/api/registryOrg/non_with_comments') + .set(nonAdminHeaders2) + .send({ ...testRegistryOrgForReviewWithComments, short_name: 'new_non_with_comments', hard_quota: 10000 }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.contain('organization was successfully updated, but joint approval is required for some fields.') + orgUUID = res.body.updated.UUID + }) + }) + it('Check to see if an ORG review was created', async () => { + await chai.request(app) + .get(`/api/review/org/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body).to.have.property('status', 'pending') + expect(res.body.target_object_uuid).to.equal(orgUUID) + expect(res.body.new_review_data.short_name).to.equal('new_non_with_comments') + reviewUUID = res.body.uuid + }) + }) + it('Check to see if the org was partially updated', async () => { + await chai.request(app) + .get(`/api/registryOrg/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.short_name).to.equal('non_with_comments') + expect(res.body.hard_quota).to.equal(10000) + }) + }) + it('Secretariat leaves a public comment on the org review', async () => { + await chai.request(app) + .post(`/api/conversation/target/${reviewUUID}`) + .set(secretariatHeaders) + .send({ + visibility: 'public', + body: 'This is a comment left by the secretariat.' + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.author_role).to.equal('Secretariat') + expect(res.body.visibility).to.equal('public') + expect(res.body.body).to.equal('This is a comment left by the secretariat.') + }) + }) + it('Secretariat leaves a private on the org review', async () => { + await chai.request(app) + .post(`/api/conversation/target/${reviewUUID}`) + .set(secretariatHeaders) + .send({ + visibility: 'private', + body: 'This is a private comment left by the secretariat.' + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.author_role).to.equal('Secretariat') + expect(res.body.visibility).to.equal('private') + expect(res.body.body).to.equal('This is a private comment left by the secretariat.') + }) + }) + it('Admin checks org review', async () => { + await chai.request(app) + .get(`/api/review/byUUID/${reviewUUID}`) + .set(nonAdminHeaders2) + .then((res, err) => { + expect(err).to.be.undefined + expect(res.body).to.have.property('conversation') + expect(res.body.conversation).to.have.length(1) + expect(res).to.have.status(200) + }) + }) + it('Secretariat checks org review', async () => { + await chai.request(app) + .get(`/api/review/byUUID/${reviewUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res.body).to.have.property('conversation') + expect(res.body.conversation).to.have.length(2) + expect(res).to.have.status(200) + }) + }) + it('Secretariat can approve the ORG review', async function () { + await chai.request(app) + .put(`/api/review/org/${reviewUUID}/approve`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.status).to.equal('approved') + }) + }) + it('Check to see if the org was fully updated', async () => { + await chai.request(app) + .get(`/api/registryOrg/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.short_name).to.equal('new_non_with_comments') + expect(res.body.hard_quota).to.equal(10000) + }) + }) + }) +}) diff --git a/test/integration-tests/review-object/reviewObjectTest.js b/test/integration-tests/review-object/reviewObjectTest.js index 44f0aec55..e2fd6c447 100644 --- a/test/integration-tests/review-object/reviewObjectTest.js +++ b/test/integration-tests/review-object/reviewObjectTest.js @@ -9,10 +9,6 @@ const app = require('../../../src/index.js') describe('Review Object Controller Integration Tests', () => { let orgUUID let reviewUUID - const reviewPayload = { - target_object_uuid: '', - new_review_data: {} - } context('Positive Tests', () => { it('Creates an organization to use for review object tests', async () => { @@ -28,13 +24,13 @@ describe('Review Object Controller Integration Tests', () => { }) it('Creates a review object for the organization', async () => { - reviewPayload.target_object_uuid = orgUUID - reviewPayload.new_review_data = constants.testRegistryOrg2 + const reviewObject = constants.testRegistryOrg2 + reviewObject.UUID = orgUUID const res = await chai .request(app) .post('/api/review/org/') .set({ ...constants.headers }) - .send(reviewPayload) + .send(constants.testRegistryOrg2) expect(res).to.have.status(200) expect(res.body).to.have.property('uuid') expect(res.body).to.have.property('target_object_uuid', orgUUID) @@ -72,16 +68,14 @@ describe('Review Object Controller Integration Tests', () => { }) it('Updates the review object with new short_name', async () => { - const updatePayload = { - new_review_data: constants.testRegistryOrg2 - } - - updatePayload.new_review_data.short_name = 'updated_org' + const reviewObject = constants.testRegistryOrg2 + reviewObject.UUID = orgUUID + reviewObject.short_name = 'updated_org' const res = await chai .request(app) .put(`/api/review/org/${reviewUUID}`) .set({ ...constants.headers }) - .send(updatePayload) + .send(reviewObject) expect(res).to.have.status(200) expect(res.body).to.have.property('uuid', reviewUUID) expect(res.body.new_review_data).to.have.property('short_name', 'updated_org') @@ -89,40 +83,6 @@ describe('Review Object Controller Integration Tests', () => { }) context('Negative Tests', () => { - it('Fails when target_object_uuid is missing', async () => { - const res = await chai - .request(app) - .post('/api/review/org/') - .set({ ...constants.headers }) - .send({ new_review_data: constants.testOrg }) - expect(res).to.have.status(400) - expect(res.body).to.have.property('message', 'Missing required field target_object_uuid') - }) - - it('Fails when new_review_data is missing', async () => { - const res = await chai - .request(app) - .post('/api/review/org/') - .set({ ...constants.headers }) - .send({ target_object_uuid: orgUUID }) - expect(res).to.have.status(400) - expect(res.body).to.have.property('message', 'Missing required field new_review_data') - }) - - it('Fails when uuid is provided in creation payload', async () => { - const res = await chai - .request(app) - .post('/api/review/org/') - .set({ ...constants.headers }) - .send({ - uuid: 'should-not-be-here', - target_object_uuid: orgUUID, - new_review_data: constants.testOrg - }) - expect(res).to.have.status(400) - expect(res.body).to.have.property('message', 'Do not pass in a uuid key when creating a review object') - }) - it('Returns 404 for non-existent review object GET', async () => { const res = await chai .request(app) @@ -130,20 +90,5 @@ describe('Review Object Controller Integration Tests', () => { .set({ ...constants.headers }) expect(res).to.have.status(404) }) - - it('Returns 404 for non-existent review object UPDATE', async () => { - const updatePayload = { - new_review_data: constants.testRegistryOrg2 - } - - updatePayload.new_review_data.short_name = 'updated_org' - const res = await chai - .request(app) - .put('/api/review/org/nonexistent-uuid') - .set({ ...constants.headers }) - .send(updatePayload) - expect(res).to.have.status(404) - expect(res.body).to.have.property('message') - }) }) }) diff --git a/test/integration-tests/user/getUserTestRegistryFlag.js b/test/integration-tests/user/getUserTestRegistryFlag.js deleted file mode 100644 index 0a8f3bead..000000000 --- a/test/integration-tests/user/getUserTestRegistryFlag.js +++ /dev/null @@ -1,118 +0,0 @@ -const chai = require('chai') -chai.use(require('chai-http')) -const expect = chai.expect - -const constants = require('../constants.js') -const app = require('../../../src/index.js') -const BASE_URL = '/api' -/** - * Unit Tests for testing User Get Request for /api/org with the `registry=true` flag - */ - -describe('Testing /api/org/ user endpoints with `registry=true`', () => { - // Testing USER GET Endpoints with `registry=true` flag - describe('Testing USER GET endpoint with `registry=true`', () => { - /* Positive Tests */ - it('secretariat users can request a list of all users', async () => { - await chai.request(app) - .get(`${BASE_URL}/users?registry=true`) - .set(constants.headers) - .send({ - }) - .then((res) => { - expect(res).to.have.status(200) - // check the fields returned - }) - }) - it('page must be a positive int', async () => { - await chai.request(app) - .get(`${BASE_URL}/registry/users?page=1`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - }) - }) - it('can retrieve user after an update', async () => { - const user = constants.nonSecretariatUserHeaders3['CVE-API-USER'] - const org = constants.nonSecretariatUserHeaders3['CVE-API-ORG'] - const newFirstName = 'testFirstName' - var oldFirstName = '' - await chai.request(app) - .get(`${BASE_URL}/registry/org/${org}/user/${user}`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - oldFirstName = res.body.name.first - }) - await chai.request(app) - .put(`${BASE_URL}/registry/org/${org}/user/${user}?name.first=${newFirstName}`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - }) - await chai.request(app) - .get(`${BASE_URL}/registry/org/${org}/user/${user}`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - expect(res.body.name.first).to.contain(newFirstName) - }) - await chai.request(app) - .put(`${BASE_URL}/registry/org/${org}/user/${user}?name.first=${oldFirstName}`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - }) - }) - }) - /* Negative Tests */ - context('Negative Test', () => { - it('regular users cannot request a list of all users', async () => { - await chai.request(app) - .get(`${BASE_URL}/registry/users`) - .set(constants.nonSecretariatUserHeaders) - .send({ - }) - .then((res) => { - expect(res).to.have.status(403) - expect(res.body.error).to.contain('SECRETARIAT_ONLY') - }) - }) - it('org admins cannot request a list of all users', async () => { - await chai.request(app) - .get(`${BASE_URL}/registry/users`) - .set(constants.nonSecretariatUserHeaders2) - .send({ - }) - .then((res) => { - expect(res).to.have.status(403) - expect(res.body.error).to.contain('SECRETARIAT_ONLY') - }) - }) - it('page must be a positive int', async () => { - // test negative int - await chai.request(app) - .get(`${BASE_URL}/registry/users?page=-1`) - .set(constants.headers) - .send({}) - .then((res) => { - expect(res).to.have.status(400) - expect(res.body.error).to.contain('BAD_INPUT') - }) - // test string - await chai.request(app) - .get(`${BASE_URL}/registry/users?page=abc`) - .set(constants.headers) - .send({}) - .then((res) => { - expect(res).to.have.status(400) - expect(res.body.error).to.contain('BAD_INPUT') - }) - }) - }) -}) diff --git a/test/unit-tests/org/orgCreateADPTest.js b/test/unit-tests/org/orgCreateADPTest.js index b5dac7401..031885474 100644 --- a/test/unit-tests/org/orgCreateADPTest.js +++ b/test/unit-tests/org/orgCreateADPTest.js @@ -6,12 +6,6 @@ const { faker } = require('@faker-js/faker') const expect = chai.expect const mongoose = require('mongoose') -const OrgRepository = require('../../../src/repositories/orgRepository.js') -const UserRepository = require('../../../src/repositories/userRepository.js') - -const RegistryOrgRepository = require('../../../src/repositories/registryOrgRepository.js') -const RegistryUserRepository = require('../../../src/repositories/registryUserRepository.js') - const { ORG_CREATE_SINGLE } = require('../../../src/controller/org.controller/org.controller.js') const CONSTANTS = require('../../../src/constants/index.js') const BaseOrgRepository = require('../../../src/repositories/baseOrgRepository.js') @@ -73,6 +67,7 @@ describe('Testing creating orgs with the ADP role', () => { // --- Method Stubbing -- sinon.stub(regOrgRepo, 'findOneByShortName').resolves(null) + sinon.stub(regOrgRepo, 'isSecretariatByShortName').resolves(true) // Stub aggregate to return an array with a fake object, so result[0] works const fakeAggregatedOrg = { UUID: 'org-uuid-123', short_name: 'fakeOrg', name: 'Fake Org Name' } diff --git a/test/unit-tests/org/orgCreateTest.js b/test/unit-tests/org/orgCreateTest.js index a99ec8396..cf098cdcb 100644 --- a/test/unit-tests/org/orgCreateTest.js +++ b/test/unit-tests/org/orgCreateTest.js @@ -16,6 +16,7 @@ const SecretariatOrgModel = require('../../../src/model/secretariatorg.js') const CNAOrgModel = require('../../../src/model/cnaorg.js') const ADPOrgModel = require('../../../src/model/adporg.js') const Org = require('../../../src/model/org.js') +const AuditRepository = require('../../../src/repositories/auditRepository.js') // Mocks for error messages and constants const { OrgControllerError } = require('../../../src/controller/org.controller/error.js') @@ -53,7 +54,7 @@ const orgFixtures = { describe('Testing the ORG_CREATE_SINGLE controller', () => { let status, json, res, next, getOrgRepository, orgRepo, getUserRepository, getBaseOrgRepository, getBaseUserRepository, - userRepo, mockSession, baseOrgRepo, baseUserRepo, fakeBaseSavedObject, saveStub, fakeLegacySavedObject, fakeMongooseDocument, fakeBaseSavedObjectCisco, fakeLegacySavedObjectCisco + userRepo, mockSession, baseOrgRepo, baseUserRepo, fakeBaseSavedObject, saveStub, fakeLegacySavedObject, fakeMongooseDocument, fakeBaseSavedObjectCisco, fakeLegacySavedObjectCisco, auditRepo // Runs before each test case beforeEach(() => { @@ -123,6 +124,8 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { saveStub = sinon.stub(SecretariatOrgModel.prototype, 'save').resolves(fakeBaseSavedObject) fakeMongooseDocument = new Org(fakeLegacySavedObject) + sinon.stub(AuditRepository.prototype, 'appendToAuditHistoryForOrg').resolves(true) + // Stub repository getters orgRepo = new OrgRepository() getOrgRepository = sinon.stub().returns(orgRepo) @@ -201,6 +204,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { sinon.stub(orgRepo, 'getOrgUUID').resolves('org-uuid-123') sinon.stub(userRepo, 'getUserUUID').resolves('user-uuid-123') sinon.stub(baseOrgRepo, 'getOrgUUID').resolves('org-uuid-123') + sinon.stub(baseOrgRepo, 'isSecretariatByShortName').resolves(true) sinon.stub(baseUserRepo, 'getUserUUID').resolves('user-uuid-123') }) diff --git a/test/unit-tests/org/orgUpdateTest.js b/test/unit-tests/org/orgUpdateTest.js index 9542b59fc..6d9f624c9 100644 --- a/test/unit-tests/org/orgUpdateTest.js +++ b/test/unit-tests/org/orgUpdateTest.js @@ -39,6 +39,10 @@ class OrgUpdatedAddingRole { return orgFixtures.owningOrg } + async isSecretariatByShortName () { + return true + } + async aggregate () { return [orgFixtures.owningOrg] } @@ -78,6 +82,10 @@ class OrgUpdatedRemovingRole { return temp } + async isSecretariatByShortName () { + return true + } + async orgExists () { return true } @@ -333,6 +341,10 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { return true } + async isSecretariatByShortName () { + return true + } + async updateOrg () { return orgFixtures.existentOrg } diff --git a/test/unit-tests/review-object/review-object.controller.test.js b/test/unit-tests/review-object/review-object.controller.test.js index 06d0416b1..fb7202807 100644 --- a/test/unit-tests/review-object/review-object.controller.test.js +++ b/test/unit-tests/review-object/review-object.controller.test.js @@ -23,6 +23,7 @@ describe('Review Object Controller', function () { } next = sinon.stub() + orgRepoStub.isSecretariatByShortName = sinon.stub().resolves(true) }) describe('getReviewObjectByOrgIdentifier', function () { @@ -65,16 +66,6 @@ describe('Review Object Controller', function () { }) describe('updateReviewObjectByReviewUUID', function () { - it('should return 400 if new_review_data is invalid', async () => { - req.params.uuid = 'some-uuid' - req.body.new_review_data = { invalid: true } - orgRepoStub.validateOrg = sinon.stub().returns({ isValid: false, errors: ['bad data'] }) - await controller.updateReviewObjectByReviewUUID(req, res, next) - expect(orgRepoStub.validateOrg.calledWith(req.body.new_review_data)).to.be.true - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Invalid new_review_data', errors: ['bad data'] })).to.be.true - }) - it('should return 404 if review object not found', async () => { const uuid = 'rev-uuid' req.params.uuid = uuid @@ -102,41 +93,11 @@ describe('Review Object Controller', function () { }) describe('createReviewObject', function () { - it('should return 400 if body contains uuid', async () => { - req.body.uuid = 'should-not-be-here' - await controller.createReviewObject(req, res, next) - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Do not pass in a uuid key when creating a review object' })).to.be.true - }) - - it('should return 400 if target_object_uuid missing', async () => { - req.body.new_review_data = { foo: 'bar' } - await controller.createReviewObject(req, res, next) - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Missing required field target_object_uuid' })).to.be.true - }) - - it('should return 400 if new_review_data missing', async () => { - req.body.target_object_uuid = 'obj-uuid' - await controller.createReviewObject(req, res, next) - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Missing required field new_review_data' })).to.be.true - }) - - it('should return 400 if new_review_data is invalid', async () => { - req.body.target_object_uuid = 'obj-uuid' - req.body.new_review_data = { bad: true } - orgRepoStub.validateOrg = sinon.stub().returns({ isValid: false, errors: ['err'] }) - await controller.createReviewObject(req, res, next) - expect(orgRepoStub.validateOrg.calledWith(req.body.new_review_data)).to.be.true - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Invalid new_review_data', errors: ['err'] })).to.be.true - }) - it('should return 500 if repo create fails', async () => { req.body.target_object_uuid = 'obj-uuid' req.body.new_review_data = { foo: 'bar' } orgRepoStub.validateOrg = sinon.stub().returns({ isValid: true }) + repoStub.validateOrg = repoStub.createReviewOrgObject = sinon.stub().resolves(undefined) await controller.createReviewObject(req, res, next) expect(repoStub.createReviewOrgObject.calledWith(req.body)).to.be.true