diff --git a/api-docs/openapi.json b/api-docs/openapi.json
index 65a528fb1..5bcc9895f 100644
--- a/api-docs/openapi.json
+++ b/api-docs/openapi.json
@@ -2077,6 +2077,14 @@
"$ref": "../schemas/registry-org/BulkDownloadOrg.json"
}
]
+ },
+ "example": {
+ "short_name": "fake_company",
+ "name": "Fake Company",
+ "hard_quota": 1000,
+ "authority": [
+ "CNA"
+ ]
}
}
}
@@ -2121,6 +2129,31 @@
"application/json": {
"schema": {
"$ref": "../schemas/registry-user/list-registry-users-response.json"
+ },
+ "example": {
+ "totalCount": 1,
+ "itemsPerPage": 100,
+ "pageCount": 1,
+ "currentPage": 1,
+ "prevPage": null,
+ "nextPage": null,
+ "users": [
+ {
+ "UUID": "fe566221-6a2c-4279-8800-4d3795325997",
+ "org_UUID": "9e243a41-352b-426a-9dfd-f664b4c71e80",
+ "username": "jdoe",
+ "name": {
+ "first": "John",
+ "last": "Doe"
+ },
+ "role": "ADMIN",
+ "is_active": true,
+ "time": {
+ "created": "2021-02-12T17:15:37.382Z",
+ "modified": "2021-02-12T17:15:37.382Z"
+ }
+ }
+ ]
}
}
}
@@ -2178,14 +2211,14 @@
}
}
},
- "/registry/org/{shortname}/id_quota": {
+ "/registry/org/{shortname}/hard_quota": {
"get": {
"tags": [
"Registry Organization"
],
"summary": "Retrieves an organization's CVE ID quota (accessible to all registered users)",
"description": "
Access Control
All registered users can access this endpoint
Expected Behavior
Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization
Secretariat: Retrieves the CVE ID quota for any organization
",
- "operationId": "orgIdQuota",
+ "operationId": "orgHardQuota",
"parameters": [
{
"name": "shortname",
@@ -2489,27 +2522,6 @@
{
"$ref": "#/components/parameters/active"
},
- {
- "$ref": "#/components/parameters/activeUserRolesAdd"
- },
- {
- "$ref": "#/components/parameters/activeUserRolesRemove"
- },
- {
- "$ref": "#/components/parameters/nameFirst"
- },
- {
- "$ref": "#/components/parameters/nameLast"
- },
- {
- "$ref": "#/components/parameters/nameMiddle"
- },
- {
- "$ref": "#/components/parameters/nameSuffix"
- },
- {
- "$ref": "#/components/parameters/newUsername"
- },
{
"$ref": "#/components/parameters/orgShortname"
},
@@ -2592,8 +2604,8 @@
"tags": [
"Registry Organization"
],
- "summary": "Updates information about the organization specified by short name (accessible to Secretariat)",
- "description": " Access Control
User must belong to an organization with the Secretariat role
Expected Behavior
Secretariat: Updates any organization's information
",
+ "summary": "Updates information about the organization specified by short name (accessible Temporarily to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role temporarily.
In the future, only the organization's admin will be able to request changes to its information.
With Joint Approval required for the following fields:
Expected Behavior
This endpoint expects a full organization object in the request body. Secretariat: Updates any organization's information
Organization Admin: Requests changes to its organization's information
- 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
",
"operationId": "orgUpdateSingle",
"parameters": [
{
@@ -2605,21 +2617,6 @@
},
"description": "The shortname of the organization"
},
- {
- "$ref": "#/components/parameters/id_quota"
- },
- {
- "$ref": "#/components/parameters/name"
- },
- {
- "$ref": "#/components/parameters/newShortname"
- },
- {
- "$ref": "#/components/parameters/active_roles_add"
- },
- {
- "$ref": "#/components/parameters/active_roles_remove"
- },
{
"$ref": "#/components/parameters/apiEntityHeader"
},
@@ -2691,6 +2688,24 @@
}
}
}
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/registry-org/update-registry-org-request.json"
+ },
+ "example": {
+ "short_name": "fake_company",
+ "name": "Fake Company",
+ "hard_quota": 1000,
+ "authority": [
+ "CNA"
+ ]
+ }
+ }
+ }
}
}
},
@@ -2897,24 +2912,32 @@
}
}
},
- "/org": {
- "get": {
+ "/registry/org/{shortname}/user/{username}/grant-role": {
+ "post": {
"tags": [
- "Organization"
+ "Registry User"
],
- "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": "Grants a role to a user (accessible to Secretariat or Org Admin)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role or be an Admin of the organization
Expected Behavior
Admin User: Grants a role to a user in the Admin's organization
Secretariat: Grants a role to a user in any organization
",
+ "operationId": "registryUserGrantRole",
"parameters": [
{
- "name": "registry",
- "in": "query",
+ "name": "shortname",
+ "in": "path",
+ "required": true,
"schema": {
"type": "string"
- }
+ },
+ "description": "The shortname of the organization"
},
{
- "$ref": "#/components/parameters/pageQuery"
+ "name": "username",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The username of the user"
},
{
"$ref": "#/components/parameters/apiEntityHeader"
@@ -2928,18 +2951,16 @@
],
"responses": {
"200": {
- "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data",
+ "description": "Role granted successfully",
"content": {
"application/json": {
"schema": {
- "oneOf": [
- {
- "$ref": "../schemas/org/list-orgs-response.json"
- },
- {
- "$ref": "../schemas/registry-org/list-registry-orgs-response.json"
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
}
- ]
+ }
}
}
}
@@ -2994,16 +3015,57 @@
}
}
}
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "role": {
+ "type": "string",
+ "enum": [
+ "ADMIN"
+ ]
+ }
+ },
+ "required": [
+ "role"
+ ]
+ }
+ }
+ }
}
- },
+ }
+ },
+ "/registry/org/{shortname}/user/{username}/revoke-role": {
"post": {
"tags": [
- "Organization"
+ "Registry User"
],
- "summary": "Creates an organization as specified in the request body (accessible to Secretariat)",
- "description": " Access Control
User must belong to an organization with the Secretariat role
Expected Behavior
Secretariat: Creates an organization
",
- "operationId": "orgCreateSingle",
+ "summary": "Revokes a role from a user (accessible to Secretariat or Org Admin)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role or be an Admin of the organization
Expected Behavior
Admin User: Revokes a role from a user in the Admin's organization
Secretariat: Revokes a role from a user in any organization
",
+ "operationId": "registryUserRevokeRole",
"parameters": [
+ {
+ "name": "shortname",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The shortname of the organization"
+ },
+ {
+ "name": "username",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The username of the user"
+ },
{
"$ref": "#/components/parameters/apiEntityHeader"
},
@@ -3016,18 +3078,16 @@
],
"responses": {
"200": {
- "description": "Returns information about the organization created",
+ "description": "Role revoked successfully",
"content": {
"application/json": {
"schema": {
- "oneOf": [
- {
- "$ref": "../schemas/org/create-org-response.json"
- },
- {
- "$ref": "../schemas/registry-org/create-registry-org-response.json"
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
}
- ]
+ }
}
}
}
@@ -3088,30 +3148,42 @@
"content": {
"application/json": {
"schema": {
- "$ref": "../schemas/org/create-org-request.json"
+ "type": "object",
+ "properties": {
+ "role": {
+ "type": "string",
+ "enum": [
+ "ADMIN"
+ ]
+ }
+ },
+ "required": [
+ "role"
+ ]
}
}
}
}
}
},
- "/org/{identifier}": {
+ "/org": {
"get": {
"tags": [
"Organization"
],
- "summary": "Retrieves information about the organization specified by short name or UUID (accessible to all registered users)",
- "description": " Access Control
All registered users can access this endpoint
Expected Behavior
Regular, CNA & Admin Users: Retrieves organization record for the specified shortname or UUID if it is the user's organization
Secretariat: Retrieves information about any organization
",
- "operationId": "orgSingle",
+ "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",
"parameters": [
{
- "name": "identifier",
- "in": "path",
- "required": true,
+ "name": "registry",
+ "in": "query",
"schema": {
"type": "string"
- },
- "description": "The shortname or UUID of the organization"
+ }
+ },
+ {
+ "$ref": "#/components/parameters/pageQuery"
},
{
"$ref": "#/components/parameters/apiEntityHeader"
@@ -3125,11 +3197,18 @@
],
"responses": {
"200": {
- "description": "Returns the organization information",
+ "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data",
"content": {
"application/json": {
"schema": {
- "$ref": "../schemas/org/get-org-response.json"
+ "oneOf": [
+ {
+ "$ref": "../schemas/org/list-orgs-response.json"
+ },
+ {
+ "$ref": "../schemas/registry-org/list-registry-orgs-response.json"
+ }
+ ]
}
}
}
@@ -3185,41 +3264,15 @@
}
}
}
- }
- },
- "/org/{shortname}": {
- "put": {
+ },
+ "post": {
"tags": [
"Organization"
],
- "summary": "Updates information about the organization specified by short name (accessible to Secretariat)",
- "description": " Access Control
User must belong to an organization with the Secretariat role
Expected Behavior
Secretariat: Updates any organization's information
",
- "operationId": "orgUpdateSingle",
+ "summary": "Creates an organization as specified in the request body (accessible to Secretariat)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
Expected Behavior
Secretariat: Creates an organization
",
+ "operationId": "orgCreateSingle",
"parameters": [
- {
- "name": "shortname",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
- },
- "description": "The shortname of the organization"
- },
- {
- "$ref": "#/components/parameters/id_quota"
- },
- {
- "$ref": "#/components/parameters/name"
- },
- {
- "$ref": "#/components/parameters/newShortname"
- },
- {
- "$ref": "#/components/parameters/active_roles_add"
- },
- {
- "$ref": "#/components/parameters/active_roles_remove"
- },
{
"$ref": "#/components/parameters/apiEntityHeader"
},
@@ -3232,11 +3285,18 @@
],
"responses": {
"200": {
- "description": "Returns information about the organization updated",
+ "description": "Returns information about the organization created",
"content": {
"application/json": {
"schema": {
- "$ref": "../schemas/org/update-org-response.json"
+ "oneOf": [
+ {
+ "$ref": "../schemas/org/create-org-response.json"
+ },
+ {
+ "$ref": "../schemas/registry-org/create-registry-org-response.json"
+ }
+ ]
}
}
}
@@ -3291,26 +3351,36 @@
}
}
}
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/org/create-org-request.json"
+ }
+ }
+ }
}
}
},
- "/org/{shortname}/id_quota": {
+ "/org/{identifier}": {
"get": {
"tags": [
"Organization"
],
- "summary": "Retrieves an organization's CVE ID quota (accessible to all registered users)",
- "description": " Access Control
All registered users can access this endpoint
Expected Behavior
Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization
Secretariat: Retrieves the CVE ID quota for any organization
",
- "operationId": "orgIdQuota",
+ "summary": "Retrieves information about the organization specified by short name or UUID (accessible to all registered users)",
+ "description": " Access Control
All registered users can access this endpoint
Expected Behavior
Regular, CNA & Admin Users: Retrieves organization record for the specified shortname or UUID if it is the user's organization
Secretariat: Retrieves information about any organization
",
+ "operationId": "orgSingle",
"parameters": [
{
- "name": "shortname",
+ "name": "identifier",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
- "description": "The shortname of the organization"
+ "description": "The shortname or UUID of the organization"
},
{
"$ref": "#/components/parameters/apiEntityHeader"
@@ -3324,11 +3394,210 @@
],
"responses": {
"200": {
- "description": "Returns the CVE ID quota for an organization",
+ "description": "Returns the organization information",
"content": {
"application/json": {
"schema": {
- "$ref": "../schemas/org/get-org-quota-response.json"
+ "$ref": "../schemas/org/get-org-response.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/org/{shortname}": {
+ "put": {
+ "tags": [
+ "Organization"
+ ],
+ "summary": "Updates information about the organization specified by short name (accessible to Secretariat)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
Expected Behavior
Secretariat: Updates any organization's information
",
+ "operationId": "orgUpdateSingle",
+ "parameters": [
+ {
+ "name": "shortname",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The shortname of the organization"
+ },
+ {
+ "$ref": "#/components/parameters/id_quota"
+ },
+ {
+ "$ref": "#/components/parameters/name"
+ },
+ {
+ "$ref": "#/components/parameters/newShortname"
+ },
+ {
+ "$ref": "#/components/parameters/active_roles_add"
+ },
+ {
+ "$ref": "#/components/parameters/active_roles_remove"
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns information about the organization updated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/org/update-org-response.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/org/{shortname}/id_quota": {
+ "get": {
+ "tags": [
+ "Organization"
+ ],
+ "summary": "Retrieves an organization's CVE ID quota (accessible to all registered users)",
+ "description": " Access Control
All registered users can access this endpoint
Expected Behavior
Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization
Secretariat: Retrieves the CVE ID quota for any organization
",
+ "operationId": "orgIdQuota",
+ "parameters": [
+ {
+ "name": "shortname",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The shortname of the organization"
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the CVE ID quota for an organization",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/org/get-org-quota-response.json"
}
}
}
@@ -4117,6 +4386,1080 @@
}
}
}
+ },
+ "/conversation": {
+ "get": {
+ "tags": [
+ "Conversation"
+ ],
+ "summary": "Retrieves all conversations (accessible to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
Expected Behavior
Secretariat: Retrieves all conversations
",
+ "operationId": "getAllConversations",
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "description": "The page of the conversation to retrieve",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns all conversations, along with pagination fields if results span multiple pages of data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/conversation/list-conversations-response.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/conversation/target/{uuid}": {
+ "get": {
+ "tags": [
+ "Conversation"
+ ],
+ "summary": "Retrieves all conversations for a specific target UUID (accessible to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
Expected Behavior
Secretariat: Retrieves all conversations for the specified target UUID
",
+ "operationId": "getConversationsForTargetUUID",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The UUID of the target entity"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "description": "The page of the conversation to retrieve",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns all conversations for the target UUID, along with pagination fields if results span multiple pages of data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/conversation/list-conversations-response.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "Conversation"
+ ],
+ "summary": "Creates a conversation for a specific target UUID (accessible to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
Expected Behavior
Secretariat: Creates a conversation for the specified target UUID
",
+ "operationId": "createConversationForTargetUUID",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The UUID of the target entity"
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the created conversation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/conversation/conversation.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "body": {
+ "type": "string",
+ "description": "The content of the conversation message"
+ }
+ },
+ "required": [
+ "body"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/review/byUUID/{uuid}": {
+ "get": {
+ "tags": [
+ "Review Object"
+ ],
+ "summary": "Retrieves a review object by its UUID (accessible to Secretariat or Admin)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role or have the Admin role
",
+ "operationId": "getReviewObjectByUUID",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The UUID of the review object"
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the review object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/review/review.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/review/org/{identifier}": {
+ "get": {
+ "tags": [
+ "Review Object"
+ ],
+ "summary": "Retrieves the PENDING review object for an organization (accessible to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
",
+ "operationId": "getReviewObjectByOrgIdentifier",
+ "parameters": [
+ {
+ "name": "identifier",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The short name or UUID of the organization"
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the pending review object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/review/review.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/review/orgs": {
+ "get": {
+ "tags": [
+ "Review Object"
+ ],
+ "summary": "Retrieves all review objects (accessible to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
",
+ "operationId": "getAllReviewObjects",
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "description": "The page of results to retrieve",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "status",
+ "in": "query",
+ "description": "Filter by review object status",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns a list of review objects",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/review/list-reviews-response.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/review/org/{identifier}/reviews": {
+ "get": {
+ "tags": [
+ "Review Object"
+ ],
+ "summary": "Retrieves the review history for an organization (accessible to Secretariat or Admin)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role or have the Admin role
",
+ "operationId": "getReviewHistoryByOrgShortNamePaginated",
+ "parameters": [
+ {
+ "name": "identifier",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The short name of the organization"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "description": "The page of results to retrieve",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "include_conversations",
+ "in": "query",
+ "description": "Whether to include conversation history",
+ "schema": {
+ "type": "boolean"
+ }
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the review history",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/review/list-reviews-response.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/review/org/{uuid}": {
+ "put": {
+ "tags": [
+ "Review Object"
+ ],
+ "summary": "Updates a review object (accessible to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
",
+ "operationId": "updateReviewObjectByReviewUUID",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The UUID of the review object"
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the updated review object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/review/review.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "The updated review data"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/review/org/{uuid}/approve": {
+ "put": {
+ "tags": [
+ "Review Object"
+ ],
+ "summary": "Approves a review object and applies changes to the organization (accessible to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
",
+ "operationId": "approveReviewObject",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The UUID of the review object"
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the updated organization",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "The updated organization object"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "Optional override data to apply instead of the review object data"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/review/org/{uuid}/reject": {
+ "put": {
+ "tags": [
+ "Review Object"
+ ],
+ "summary": "Rejects a review object (accessible to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
",
+ "operationId": "rejectReviewObject",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The UUID of the review object"
+ },
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the rejected review object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/review/review.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/review/org/": {
+ "post": {
+ "tags": [
+ "Review Object"
+ ],
+ "summary": "Creates a new review object (accessible to Secretariat only)",
+ "description": " Access Control
User must belong to an organization with the Secretariat role
",
+ "operationId": "createReviewObject",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/apiEntityHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiUserHeader"
+ },
+ {
+ "$ref": "#/components/parameters/apiSecretHeader"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the created review object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/review/review.json"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/bad-request.json"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not Authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "The review object data"
+ }
+ }
+ }
+ }
+ }
}
},
"components": {
diff --git a/schemas/conversation/conversation.json b/schemas/conversation/conversation.json
new file mode 100644
index 000000000..6d9d9fb91
--- /dev/null
+++ b/schemas/conversation/conversation.json
@@ -0,0 +1,63 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://cve.mitre.org/schema/conversation/conversation.json",
+ "type": "object",
+ "title": "Conversation",
+ "description": "JSON Schema for a conversation message",
+ "properties": {
+ "UUID": {
+ "type": "string",
+ "description": "Unique identifier for the conversation message"
+ },
+ "target_uuid": {
+ "type": "string",
+ "description": "UUID of the target entity (e.g., Organization)"
+ },
+ "previous_conversation_uuid": {
+ "type": "string",
+ "description": "UUID of the previous message in the conversation thread"
+ },
+ "next_conversation_uuid": {
+ "type": "string",
+ "description": "UUID of the next message in the conversation thread"
+ },
+ "author_id": {
+ "type": "string",
+ "description": "UUID of the message author"
+ },
+ "author_name": {
+ "type": "string",
+ "description": "Name of the message author"
+ },
+ "author_role": {
+ "type": "string",
+ "description": "Role of the message author"
+ },
+ "visibility": {
+ "type": "string",
+ "description": "Visibility level of the message"
+ },
+ "body": {
+ "type": "string",
+ "description": "Content of the message"
+ },
+ "posted_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Timestamp when the message was posted"
+ },
+ "last_updated": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Timestamp when the message was last updated"
+ }
+ },
+ "required": [
+ "UUID",
+ "target_uuid",
+ "author_id",
+ "author_name",
+ "body",
+ "posted_at"
+ ]
+}
diff --git a/schemas/conversation/list-conversations-response.json b/schemas/conversation/list-conversations-response.json
new file mode 100644
index 000000000..a92694e99
--- /dev/null
+++ b/schemas/conversation/list-conversations-response.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://cve.mitre.org/schema/conversation/list-conversations-response.json",
+ "type": "object",
+ "title": "List Conversations Response",
+ "description": "JSON Schema for a list of conversation messages",
+ "properties": {
+ "totalCount": {
+ "type": "integer",
+ "description": "Total number of items"
+ },
+ "itemsPerPage": {
+ "type": "integer",
+ "description": "Number of items per page"
+ },
+ "pageCount": {
+ "type": "integer",
+ "description": "Total number of pages"
+ },
+ "currentPage": {
+ "type": "integer",
+ "description": "Current page number"
+ },
+ "prevPage": {
+ "type": ["integer", "null"],
+ "description": "Previous page number"
+ },
+ "nextPage": {
+ "type": ["integer", "null"],
+ "description": "Next page number"
+ },
+ "conversations": {
+ "type": "array",
+ "items": {
+ "$ref": "conversation.json"
+ },
+ "description": "List of conversation messages"
+ }
+ }
+}
diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json
index 3ee9bd62e..6f0bfb0ec 100644
--- a/schemas/registry-org/create-registry-org-response.json
+++ b/schemas/registry-org/create-registry-org-response.json
@@ -33,7 +33,7 @@
},
"cve_program_org_function": {
"type": "string",
- "enum": ["CNA", "ADP", "Root", "Secretariat"],
+ "enum": ["CNA", "ADP", "Secretariat"],
"description": "The organization's function within the CVE program"
},
"authority": {
@@ -43,7 +43,7 @@
"type": "array",
"items": {
"type": "string",
- "enum": ["CNA", "ADP", "Root", "Secretariat"]
+ "enum": ["CNA", "ADP", "Secretariat"]
}
}
},
diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json
index e0816a144..f0ae01dca 100644
--- a/schemas/registry-org/get-registry-org-response.json
+++ b/schemas/registry-org/get-registry-org-response.json
@@ -9,14 +9,14 @@
"type": "string",
"description": "Unique identifier for the organization"
},
- "long_name": {
- "type": "string",
- "description": "Full name of the organization"
- },
"short_name": {
"type": "string",
"description": "Short name or acronym of the organization"
},
+ "long_name": {
+ "type": "string",
+ "description": "Full name of the organization"
+ },
"aliases": {
"type": "array",
"items": {
@@ -24,23 +24,22 @@
},
"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",
- "items": {
- "type": "string",
- "enum": ["CNA", "ADP", "Root", "Secretariat"]
- }
- }
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "CNA",
+ "ADP",
+ "Secretariat",
+ "BULK_DOWNLOAD"
+ ]
},
- "required": ["active_roles"]
+ "description": "The organization's function within the CVE program"
+ },
+ "root_or_tlr": {
+ "type": "boolean",
+ "description": "Indicates if the organization is a root or top-level root"
},
"reports_to": {
"type": ["string", "null"],
@@ -53,36 +52,19 @@
},
"description": "UUIDs of organizations overseen by this organization"
},
- "root_or_tlr": {
- "type": "boolean",
- "description": "Indicates if the organization is a root or top-level root"
- },
"users": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "UUIDs of users associated with this organization"
- },
- "charter_or_scope": {
- "type": "string",
- "description": "Description of the organization's charter or scope"
- },
- "disclosure_policy": {
- "type": "string",
- "description": "The organization's disclosure policy"
- },
- "product_list": {
- "type": "string",
- "description": "List of products associated with the organization"
- },
- "soft_quota": {
- "type": "integer",
- "description": "Soft quota for CVE IDs"
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "UUIDs of users associated with this organization"
},
- "hard_quota": {
- "type": "integer",
- "description": "Hard quota for CVE IDs"
+ "admins": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "UUIDs of admin users"
},
"contact_info": {
"type": "object",
@@ -106,13 +88,6 @@
"type": "string",
"description": "Point of contact phone number"
},
- "admins": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "UUIDs of admin users"
- },
"org_email": {
"type": "string",
"format": "email",
@@ -124,7 +99,43 @@
"description": "Organization's website URL"
}
},
- "required": ["poc", "poc_email", "admins", "org_email"]
+ "required": [
+ "poc",
+ "poc_email",
+ "org_email"
+ ]
+ },
+ "cna_role_type": {
+ "type": "string",
+ "description": "Type of CNA role"
+ },
+ "cna_country": {
+ "type": "string",
+ "description": "Country of the CNA"
+ },
+ "vulnerability_advisory_locations": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Locations of vulnerability advisories"
+ },
+ "advisory_location_require_credentials": {
+ "type": "boolean",
+ "description": "Indicates if advisory locations require credentials"
+ },
+ "industry": {
+ "type": "string",
+ "description": "Industry sector of the organization"
+ },
+ "tl_root_start_date": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Start date for Top-Level Root role"
+ },
+ "is_cna_discussion_list": {
+ "type": "boolean",
+ "description": "Indicates if part of the CNA discussion list"
},
"in_use": {
"type": "boolean",
@@ -139,19 +150,36 @@
"type": "string",
"format": "date-time",
"description": "Timestamp of the last update to the organization data"
+ },
+ "hard_quota": {
+ "type": "integer",
+ "description": "Hard quota for CVE IDs"
+ },
+ "soft_quota": {
+ "type": "integer",
+ "description": "Soft quota for CVE IDs"
+ },
+ "charter_or_scope": {
+ "type": "string",
+ "description": "Description of the organization's charter or scope"
+ },
+ "disclosure_policy": {
+ "type": "string",
+ "description": "The organization's disclosure policy"
+ },
+ "product_list": {
+ "type": "string",
+ "description": "List of products associated with the organization"
+ },
+ "conversation": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "body": { "type": "string" }
+ }
+ },
+ "description": "List of conversation messages associated with the organization"
}
- },
- "required": [
- "UUID",
- "long_name",
- "short_name",
- "cve_program_org_function",
- "authority",
- "root_or_tlr",
- "users",
- "contact_info",
- "in_use",
- "created",
- "last_updated"
- ]
+ }
}
\ No newline at end of file
diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json
index 41c30f111..626d5b443 100644
--- a/schemas/registry-org/list-registry-orgs-response.json
+++ b/schemas/registry-org/list-registry-orgs-response.json
@@ -5,7 +5,7 @@
"title": "CVE Registry Orgs List",
"description": "JSON Schema for list of CVE Registry Orgs",
"properties": {
- "totalCount": {
+ "totalCount": {
"type": "integer",
"format": "int32"
},
@@ -38,14 +38,14 @@
"type": "string",
"description": "Unique identifier for the organization"
},
- "long_name": {
- "type": "string",
- "description": "Full name of the organization"
- },
"short_name": {
"type": "string",
"description": "Short name or acronym of the organization"
},
+ "long_name": {
+ "type": "string",
+ "description": "Full name of the organization"
+ },
"aliases": {
"type": "array",
"items": {
@@ -53,23 +53,22 @@
},
"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",
- "items": {
- "type": "string",
- "enum": ["CNA", "ADP", "Root", "Secretariat"]
- }
- }
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "CNA",
+ "ADP",
+ "Secretariat",
+ "BULK_DOWNLOAD"
+ ]
},
- "required": ["active_roles"]
+ "description": "The organization's function within the CVE program"
+ },
+ "root_or_tlr": {
+ "type": "boolean",
+ "description": "Indicates if the organization is a root or top-level root"
},
"reports_to": {
"type": ["string", "null"],
@@ -82,10 +81,6 @@
},
"description": "UUIDs of organizations overseen by this organization"
},
- "root_or_tlr": {
- "type": "boolean",
- "description": "Indicates if the organization is a root or top-level root"
- },
"users": {
"type": "array",
"items": {
@@ -93,25 +88,12 @@
},
"description": "UUIDs of users associated with this organization"
},
- "charter_or_scope": {
- "type": "string",
- "description": "Description of the organization's charter or scope"
- },
- "disclosure_policy": {
- "type": "string",
- "description": "The organization's disclosure policy"
- },
- "product_list": {
- "type": "string",
- "description": "List of products associated with the organization"
- },
- "soft_quota": {
- "type": "integer",
- "description": "Soft quota for CVE IDs"
- },
- "hard_quota": {
- "type": "integer",
- "description": "Hard quota for CVE IDs"
+ "admins": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "UUIDs of admin users"
},
"contact_info": {
"type": "object",
@@ -135,13 +117,6 @@
"type": "string",
"description": "Point of contact phone number"
},
- "admins": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "UUIDs of admin users"
- },
"org_email": {
"type": "string",
"format": "email",
@@ -153,7 +128,43 @@
"description": "Organization's website URL"
}
},
- "required": ["poc", "poc_email", "admins", "org_email"]
+ "required": [
+ "poc",
+ "poc_email",
+ "org_email"
+ ]
+ },
+ "cna_role_type": {
+ "type": "string",
+ "description": "Type of CNA role"
+ },
+ "cna_country": {
+ "type": "string",
+ "description": "Country of the CNA"
+ },
+ "vulnerability_advisory_locations": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Locations of vulnerability advisories"
+ },
+ "advisory_location_require_credentials": {
+ "type": "boolean",
+ "description": "Indicates if advisory locations require credentials"
+ },
+ "industry": {
+ "type": "string",
+ "description": "Industry sector of the organization"
+ },
+ "tl_root_start_date": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Start date for Top-Level Root role"
+ },
+ "is_cna_discussion_list": {
+ "type": "boolean",
+ "description": "Indicates if part of the CNA discussion list"
},
"in_use": {
"type": "boolean",
@@ -168,9 +179,39 @@
"type": "string",
"format": "date-time",
"description": "Timestamp of the last update to the organization data"
+ },
+ "hard_quota": {
+ "type": "integer",
+ "description": "Hard quota for CVE IDs"
+ },
+ "soft_quota": {
+ "type": "integer",
+ "description": "Soft quota for CVE IDs"
+ },
+ "charter_or_scope": {
+ "type": "string",
+ "description": "Description of the organization's charter or scope"
+ },
+ "disclosure_policy": {
+ "type": "string",
+ "description": "The organization's disclosure policy"
+ },
+ "product_list": {
+ "type": "string",
+ "description": "List of products associated with the organization"
+ },
+ "conversation": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "body": { "type": "string" }
+ }
+ },
+ "description": "List of conversation messages associated with the organization"
}
}
}
- }
- }
+ }
+ }
}
\ No newline at end of file
diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json
index e25fff68b..bae2bf446 100644
--- a/schemas/registry-org/update-registry-org-request.json
+++ b/schemas/registry-org/update-registry-org-request.json
@@ -22,7 +22,7 @@
},
"cve_program_org_function": {
"type": "string",
- "enum": ["CNA", "ADP", "Root", "Secretariat"],
+ "enum": ["CNA", "ADP", "Secretariat"],
"description": "The organization's function within the CVE program"
},
"authority": {
@@ -32,7 +32,7 @@
"type": "array",
"items": {
"type": "string",
- "enum": ["CNA", "ADP", "Root", "Secretariat"]
+ "enum": ["CNA", "ADP", "Secretariat"]
}
}
},
diff --git a/schemas/registry-user/get-registry-user-response.json b/schemas/registry-user/get-registry-user-response.json
index adfa03e53..7b23db936 100644
--- a/schemas/registry-user/get-registry-user-response.json
+++ b/schemas/registry-user/get-registry-user-response.json
@@ -38,6 +38,11 @@
"last"
]
},
+ "role": {
+ "type": "string",
+ "enum": ["ADMIN"],
+ "description": "The role of the user in the organization. Currently only 'ADMIN' is supported."
+ },
"org_affiliations": {
"type": "array",
"items": {
diff --git a/schemas/registry-user/list-registry-users-response.json b/schemas/registry-user/list-registry-users-response.json
index eb6216914..22bf87d65 100644
--- a/schemas/registry-user/list-registry-users-response.json
+++ b/schemas/registry-user/list-registry-users-response.json
@@ -66,7 +66,8 @@
},
"role": {
"type": "string",
- "description": "Unique identifier for the user"
+ "enum": ["ADMIN"],
+ "description": "The role of the user in the organization. Currently only 'ADMIN' is supported."
},
"secret": {
"type": "string",
diff --git a/schemas/review/list-reviews-response.json b/schemas/review/list-reviews-response.json
new file mode 100644
index 000000000..b59cc61eb
--- /dev/null
+++ b/schemas/review/list-reviews-response.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://cve.mitre.org/schema/review/list-reviews-response.json",
+ "type": "object",
+ "title": "List Review Objects Response",
+ "description": "JSON Schema for a list of review objects",
+ "properties": {
+ "totalCount": {
+ "type": "integer",
+ "description": "Total number of items"
+ },
+ "itemsPerPage": {
+ "type": "integer",
+ "description": "Number of items per page"
+ },
+ "pageCount": {
+ "type": "integer",
+ "description": "Total number of pages"
+ },
+ "currentPage": {
+ "type": "integer",
+ "description": "Current page number"
+ },
+ "prevPage": {
+ "type": ["integer", "null"],
+ "description": "Previous page number"
+ },
+ "nextPage": {
+ "type": ["integer", "null"],
+ "description": "Next page number"
+ },
+ "review_objects": {
+ "type": "array",
+ "items": {
+ "$ref": "review.json"
+ },
+ "description": "List of review objects"
+ }
+ }
+}
diff --git a/schemas/review/review.json b/schemas/review/review.json
new file mode 100644
index 000000000..2be45fdf0
--- /dev/null
+++ b/schemas/review/review.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://cve.mitre.org/schema/review/review.json",
+ "type": "object",
+ "title": "Review Object",
+ "description": "JSON Schema for a review object",
+ "properties": {
+ "uuid": {
+ "type": "string",
+ "description": "Unique identifier for the review object"
+ },
+ "target_object_uuid": {
+ "type": "string",
+ "description": "UUID of the target object being reviewed"
+ },
+ "status": {
+ "type": "string",
+ "description": "Status of the review object (e.g., PENDING, APPROVED, REJECTED)"
+ },
+ "new_review_data": {
+ "type": "object",
+ "description": "The data proposed in the review"
+ },
+ "created": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Timestamp when the review object was created"
+ },
+ "last_updated": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Timestamp when the review object was last updated"
+ }
+ },
+ "required": [
+ "uuid",
+ "target_object_uuid",
+ "status",
+ "new_review_data",
+ "created",
+ "last_updated"
+ ]
+}
diff --git a/src/controller/audit.controller/audit.controller.js b/src/controller/audit.controller/audit.controller.js
index 771b9a66c..dc1c8c8a5 100644
--- a/src/controller/audit.controller/audit.controller.js
+++ b/src/controller/audit.controller/audit.controller.js
@@ -78,7 +78,7 @@ async function createAuditDocumentForOrg (req, res, next) {
body.target_uuid,
entry.audit_object,
entry.change_author,
- { session }
+ { session, upsert: true }
)
}
} else {
@@ -87,7 +87,7 @@ async function createAuditDocumentForOrg (req, res, next) {
body.target_uuid,
body.audit_object || {},
body.change_author || req.ctx.org,
- { session }
+ { session, upsert: true }
)
}
diff --git a/src/controller/conversation.controller/conversation.controller.js b/src/controller/conversation.controller/conversation.controller.js
index 0ed9a2c97..73d5335bf 100644
--- a/src/controller/conversation.controller/conversation.controller.js
+++ b/src/controller/conversation.controller/conversation.controller.js
@@ -23,7 +23,7 @@ async function getConversationsForTargetUUID (req, res, next) {
const repo = req.ctx.repositories.getConversationRepository()
const targetUUID = req.params.uuid
- const response = await repo.getAllByTargetUUID(targetUUID)
+ const response = await repo.getAllByTargetUUID(targetUUID, true)
return res.status(200).json(response)
}
@@ -46,15 +46,7 @@ async function createConversationForTargetUUID (req, res, next) {
return res.status(400).json({ message: 'Missing required field body' })
}
- const conversationBody = {
- target_uuid: targetUUID,
- author_id: user.UUID,
- author_name: [user.name.first, user.name.last].join(' '),
- author_role: 'Secretariat',
- visibility: body.visibility ? body.visibility.toLowerCase() : 'private',
- body: body.body
- }
- const result = await repo.createConversation(conversationBody, { session })
+ const result = await repo.createConversation(targetUUID, body, user, true, { session })
await session.commitTransaction()
if (!result) {
return res.status(500).json({ message: 'Failed to create conversation' })
@@ -81,22 +73,8 @@ async function createConversationForTargetUUID (req, res, next) {
}
}
-async function updateMessage (req, res, next) {
- const repo = req.ctx.repositories.getConversationRepository()
- 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, targetUUID)
- return res.status(200).json(result)
-}
-
module.exports = {
getAllConversations,
getConversationsForTargetUUID,
- createConversationForTargetUUID,
- updateMessage
+ createConversationForTargetUUID
}
diff --git a/src/controller/conversation.controller/index.js b/src/controller/conversation.controller/index.js
index d2a8c44e0..421cc8e5e 100644
--- a/src/controller/conversation.controller/index.js
+++ b/src/controller/conversation.controller/index.js
@@ -7,6 +7,76 @@ const CONSTANTS = getConstants()
// Get all conversations - SEC only
router.get('/conversation',
+ /*
+ #swagger.tags = ['Conversation']
+ #swagger.operationId = 'getAllConversations'
+ #swagger.summary = "Retrieves all conversations (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
+ Expected Behavior
+ Secretariat: Retrieves all conversations
"
+ #swagger.parameters['page'] = {
+ in: 'query',
+ description: 'The page of the conversation to retrieve',
+ type: 'integer'
+ }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Returns all conversations, along with pagination fields if results span multiple pages of data',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/conversation/list-conversations-response.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
mw.validateUser,
mw.onlySecretariat,
query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }),
@@ -17,6 +87,77 @@ router.get('/conversation',
// Get all conversations for target UUID - SEC only
router.get('/conversation/target/:uuid',
+ /*
+ #swagger.tags = ['Conversation']
+ #swagger.operationId = 'getConversationsForTargetUUID'
+ #swagger.summary = "Retrieves all conversations for a specific target UUID (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
+ Expected Behavior
+ Secretariat: Retrieves all conversations for the specified target UUID
"
+ #swagger.parameters['uuid'] = { description: 'The UUID of the target entity' }
+ #swagger.parameters['page'] = {
+ in: 'query',
+ description: 'The page of the conversation to retrieve',
+ type: 'integer'
+ }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Returns all conversations for the target UUID, along with pagination fields if results span multiple pages of data',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/conversation/list-conversations-response.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
mw.validateUser,
mw.onlySecretariat,
query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }),
@@ -27,18 +168,93 @@ router.get('/conversation/target/:uuid',
// Post conversation for target UUID - SEC only
router.post('/conversation/target/:uuid',
+ /*
+ #swagger.tags = ['Conversation']
+ #swagger.operationId = 'createConversationForTargetUUID'
+ #swagger.summary = "Creates a conversation for a specific target UUID (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
+ Expected Behavior
+ Secretariat: Creates a conversation for the specified target UUID
"
+ #swagger.parameters['uuid'] = { description: 'The UUID of the target entity' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.requestBody = {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ body: {
+ type: 'string',
+ description: 'The content of the conversation message'
+ }
+ },
+ required: ['body']
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Returns the created conversation',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/conversation/conversation.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
mw.validateUser,
mw.onlySecretariat,
param(['uuid']).isUUID(4),
controller.createConversationForTargetUUID
)
-// Update conversation message - SEC only
-router.put('/conversation/:uuid/message',
- mw.validateUser,
- mw.onlySecretariat,
- param(['uuid']).isUUID(4),
- controller.updateMessage
-)
-
module.exports = router
diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js
index a9ad243c0..1cd686ac5 100644
--- a/src/controller/org.controller/index.js
+++ b/src/controller/org.controller/index.js
@@ -3,6 +3,8 @@ const router = express.Router()
const mw = require('../../middleware/middleware')
const errorMsgs = require('../../middleware/errorMessages')
const controller = require('./org.controller')
+const registryOrgController = require('../registry-org.controller/registry-org.controller.js')
+const registryUserController = require('../registry-user.controller/registry-user.controller.js')
const { body, param, query } = require('express-validator')
const { parseGetParams, parsePostParams, parsePutParams, parseError, isUserRole, isValidUsername, isOrgRole, validateUpdateOrgParameters } = require('./org.middleware')
// Only God and Javascript know swhy its saying it is not used when it is.....
@@ -86,7 +88,7 @@ router.get('/registry/org',
query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }),
parseError,
parseGetParams,
- controller.ORG_ALL
+ registryOrgController.ALL_ORGS
)
router.get('/registry/org/:shortname/users',
@@ -113,6 +115,31 @@ router.get('/registry/org/:shortname/users',
"application/json": {
schema: {
$ref: '../schemas/registry-user/list-registry-users-response.json'
+ },
+ example: {
+ totalCount: 1,
+ itemsPerPage: 100,
+ pageCount: 1,
+ currentPage: 1,
+ prevPage: null,
+ nextPage: null,
+ users: [
+ {
+ "UUID": "fe566221-6a2c-4279-8800-4d3795325997",
+ "org_UUID": "9e243a41-352b-426a-9dfd-f664b4c71e80",
+ "username": "jdoe",
+ "name": {
+ "first": "John",
+ "last": "Doe"
+ },
+ "role": "ADMIN",
+ "is_active": true,
+ "time": {
+ "created": "2021-02-12T17:15:37.382Z",
+ "modified": "2021-02-12T17:15:37.382Z"
+ }
+ }
+ ]
}
}
}
@@ -166,12 +193,12 @@ router.get('/registry/org/:shortname/users',
query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }),
parseError,
parseGetParams,
- controller.USER_ALL)
+ registryOrgController.USER_ALL)
-router.get('/registry/org/:shortname/id_quota',
+router.get('/registry/org/:shortname/hard_quota',
/*
#swagger.tags = ['Registry Organization']
- #swagger.operationId = 'orgIdQuota'
+ #swagger.operationId = 'orgHardQuota'
#swagger.summary = "Retrieves an organization's CVE ID quota (accessible to all registered users)"
#swagger.description = "
Access Control
@@ -317,8 +344,9 @@ router.get('/registry/org/:identifier',
query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }),
parseError,
parseGetParams,
- controller.ORG_SINGLE
+ registryOrgController.SINGLE_ORG
)
+
router.get('/registry/org/:shortname/user/:username',
/*
#swagger.tags = ['Registry User']
@@ -401,8 +429,9 @@ router.get('/registry/org/:shortname/user/:username',
query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }),
parseError,
parseGetParams,
- controller.USER_SINGLE
+ registryUserController.SINGLE_USER
)
+
router.post('/registry/org',
/*
#swagger.tags = ['Registry Organization']
@@ -429,6 +458,12 @@ router.post('/registry/org',
{ $ref: '../schemas/registry-org/ADPOrg.json' },
{ $ref: '../schemas/registry-org/BulkDownloadOrg.json' }
]
+ },
+ example: {
+ short_name: 'fake_company',
+ name: 'Fake Company',
+ hard_quota: 1000,
+ authority: ['CNA']
}
}
}
@@ -490,30 +525,67 @@ router.post('/registry/org',
query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }),
parsePostParams,
parseError,
- controller.ORG_CREATE_SINGLE
+ registryOrgController.CREATE_ORG
)
router.put('/registry/org/:shortname',
/*
#swagger.tags = ['Registry Organization']
#swagger.operationId = 'orgUpdateSingle'
- #swagger.summary = "Updates information about the organization specified by short name (accessible to Secretariat)"
+ #swagger.summary = "Updates information about the organization specified by short name (accessible Temporarily to Secretariat only)"
#swagger.description = "
Access Control
- User must belong to an organization with the Secretariat role
+ User must belong to an organization with the Secretariat role temporarily.
+ In the future, only the organization's admin will be able to request changes to its information.
+ With Joint Approval required for the following fields:
Expected Behavior
- Secretariat: Updates any organization's information
"
+ This endpoint expects a full organization object in the request body.
+ Secretariat: Updates any organization's information
+ Organization Admin: Requests changes to its organization's information
+
+ - 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
+
"
#swagger.parameters['shortname'] = { description: 'The shortname of the organization' }
#swagger.parameters['$ref'] = [
- '#/components/parameters/id_quota',
- '#/components/parameters/name',
- '#/components/parameters/newShortname',
- '#/components/parameters/active_roles_add',
- '#/components/parameters/active_roles_remove',
'#/components/parameters/apiEntityHeader',
'#/components/parameters/apiUserHeader',
'#/components/parameters/apiSecretHeader'
]
+ #swagger.requestBody = {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '../schemas/registry-org/update-registry-org-request.json'
+ },
+ example: {
+ short_name: 'fake_company',
+ name: 'Fake Company',
+ hard_quota: 1000,
+ authority: ['CNA']
+ }
+ }
+ }
+ }
#swagger.responses[200] = {
description: 'Returns information about the organization updated',
content: {
@@ -567,11 +639,9 @@ router.put('/registry/org/:shortname',
*/
mw.useRegistry(),
mw.validateUser,
- mw.onlySecretariat,
- validateUpdateOrgParameters(),
parseError,
parsePutParams,
- controller.ORG_UPDATE_SINGLE
+ registryOrgController.UPDATE_ORG
)
router.post('/registry/org/:shortname/user',
@@ -668,8 +738,9 @@ router.post('/registry/org/:shortname/user',
.custom(isUserRole),
parseError,
parsePostParams,
- controller.USER_CREATE_SINGLE
+ registryOrgController.USER_CREATE_SINGLE
)
+
router.put('/registry/org/:shortname/user/:username',
/*
#swagger.tags = ['Registry User']
@@ -686,13 +757,6 @@ router.put('/registry/org/:shortname/user/:username',
#swagger.parameters['username'] = { description: 'The username of the user' }
#swagger.parameters['$ref'] = [
'#/components/parameters/active',
- '#/components/parameters/activeUserRolesAdd',
- '#/components/parameters/activeUserRolesRemove',
- '#/components/parameters/nameFirst',
- '#/components/parameters/nameLast',
- '#/components/parameters/nameMiddle',
- '#/components/parameters/nameSuffix',
- '#/components/parameters/newUsername',
'#/components/parameters/orgShortname',
'#/components/parameters/apiEntityHeader',
'#/components/parameters/apiUserHeader',
@@ -750,33 +814,9 @@ router.put('/registry/org/:shortname/user/:username',
mw.useRegistry(),
mw.validateUser,
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'])
- }),
- query(['active', 'new_username', 'org_short_name', 'name.first', 'name.last', 'name.middle',
- '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 }),
- query(['new_username']).optional().isString().trim().notEmpty().custom(isValidUsername),
- query(['org_short_name']).optional().isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }),
- query(['name.first']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_FIRSTNAME_LENGTH }).withMessage(errorMsgs.FIRSTNAME_LENGTH),
- query(['name.last']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_LASTNAME_LENGTH }).withMessage(errorMsgs.LASTNAME_LENGTH),
- query(['name.middle']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_MIDDLENAME_LENGTH }).withMessage(errorMsgs.MIDDLENAME_LENGTH),
- query(['name.suffix']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_SUFFIX_LENGTH }).withMessage(errorMsgs.SUFFIX_LENGTH),
- query(['active_roles.add']).optional().toArray()
- .custom(isFlatStringArray)
- .bail()
- .customSanitizer(toUpperCaseArray)
- .custom(isUserRole).withMessage(errorMsgs.USER_ROLES),
- query(['active_roles.remove']).optional().toArray()
- .custom(isFlatStringArray)
- .customSanitizer(toUpperCaseArray)
- .custom(isUserRole).withMessage(errorMsgs.USER_ROLES),
parseError,
parsePutParams,
- controller.USER_UPDATE_SINGLE)
+ registryUserController.UPDATE_USER)
router.put('/registry/org/:shortname/user/:username/reset_secret',
/*
@@ -854,6 +894,190 @@ router.put('/registry/org/:shortname/user/:username/reset_secret',
controller.USER_RESET_SECRET
)
+router.post('/registry/org/:shortname/user/:username/grant-role',
+ /*
+ #swagger.tags = ['Registry User']
+ #swagger.operationId = 'registryUserGrantRole'
+ #swagger.summary = "Grants a role to a user (accessible to Secretariat or Org Admin)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role or be an Admin of the organization
+ Expected Behavior
+ Admin User: Grants a role to a user in the Admin's organization
+ Secretariat: Grants a role to a user in any organization
"
+ #swagger.parameters['shortname'] = { description: 'The shortname of the organization' }
+ #swagger.parameters['username'] = { description: 'The username of the user' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.requestBody = {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ role: {
+ type: 'string',
+ enum: ['ADMIN']
+ }
+ },
+ required: ['role']
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Role granted successfully',
+ content: {
+ "application/json": {
+ schema: { type: 'object', properties: { message: { type: 'string' } } }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ // mw.onlyOrgWithPartnerRole, // This might be too restrictive if we want Secretariat to do it for any org type
+ parseError,
+ parsePostParams,
+ registryUserController.GRANT_ROLE
+)
+
+router.post('/registry/org/:shortname/user/:username/revoke-role',
+ /*
+ #swagger.tags = ['Registry User']
+ #swagger.operationId = 'registryUserRevokeRole'
+ #swagger.summary = "Revokes a role from a user (accessible to Secretariat or Org Admin)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role or be an Admin of the organization
+ Expected Behavior
+ Admin User: Revokes a role from a user in the Admin's organization
+ Secretariat: Revokes a role from a user in any organization
"
+ #swagger.parameters['shortname'] = { description: 'The shortname of the organization' }
+ #swagger.parameters['username'] = { description: 'The username of the user' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.requestBody = {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ role: {
+ type: 'string',
+ enum: ['ADMIN']
+ }
+ },
+ required: ['role']
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Role revoked successfully',
+ content: {
+ "application/json": {
+ schema: { type: 'object', properties: { message: { type: 'string' } } }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ // mw.onlyOrgWithPartnerRole,
+ parseError,
+ parsePostParams,
+ registryUserController.REVOKE_ROLE
+)
+
router.get('/org',
/*
#swagger.tags = ['Organization']
diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js
index af9baa82b..05b9fc572 100644
--- a/src/controller/org.controller/org.controller.js
+++ b/src/controller/org.controller/org.controller.js
@@ -7,9 +7,14 @@ const error = new errors.OrgControllerError()
const validateUUID = require('uuid').validate
/**
- * Get the details of all orgs
- * Called by GET /api/registry/org, GET /api/org
- **/
+ * Get the details of all orgs.
+ * Called by GET /api/org
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
+ */
async function getOrgs (req, res, next) {
try {
const session = await mongoose.startSession()
@@ -28,7 +33,7 @@ async function getOrgs (req, res, next) {
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
try {
- returnValue = await repo.getAllOrgs({ ...options, session }, !req.useRegistry)
+ returnValue = await repo.getAllOrgs({ ...options, session }, true)
} finally {
await session.endSession()
}
@@ -41,9 +46,16 @@ async function getOrgs (req, res, next) {
}
/**
- * Get the details of a single org for the specified shortname/UUID
- * Called by GET /api/registry/org/{identifier}, GET /api/org/{identifier}
- **/
+ * Get the details of a single org for the specified shortname/UUID.
+ * Called by GET /api/org/{identifier}
+ *
+ * When Switched over to user registry only - This to be deleted
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
+ */
async function getOrg (req, res, next) {
try {
const session = await mongoose.startSession()
@@ -51,20 +63,21 @@ async function getOrg (req, res, next) {
const requesterOrgShortName = req.ctx.org
const identifier = req.ctx.params.identifier
const identifierIsUUID = validateUUID(identifier)
+ const returnLegacyFormat = true
let returnValue
try {
session.startTransaction()
- const requesterOrg = await repo.findOneByShortName(requesterOrgShortName, { session }, !req.useRegistry)
+ const requesterOrg = await repo.findOneByShortName(requesterOrgShortName, { session }, returnLegacyFormat)
const requesterOrgIdentifier = identifierIsUUID ? requesterOrg.UUID : requesterOrgShortName
- const isSecretariat = await repo.isSecretariat(requesterOrg, { session }, !req.useRegistry)
+ const isSecretariat = await repo.isSecretariat(requesterOrg, { session }, returnLegacyFormat)
if (requesterOrgIdentifier !== identifier && !isSecretariat) {
logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' })
return res.status(403).json(error.notSameOrgOrSecretariat())
}
- returnValue = await repo.getOrg(identifier, identifierIsUUID, { session }, !req.useRegistry)
+ returnValue = await repo.getOrg(identifier, identifierIsUUID, { session }, returnLegacyFormat)
} catch (error) {
await session.abortTransaction()
// Handle the specific error thrown by BaseOrgRepository.createOrg
@@ -88,9 +101,14 @@ async function getOrg (req, res, next) {
}
/**
- * Get the details of all users from an org given the specified shortname
- * Called by GET /api/registry/org/{shortname}/users, GET /api/org/{shortname}/users
- **/
+ * Get the details of all users from an org given the specified shortname.
+ * Called by GET /api/org/{shortname}/users
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
+ */
async function getUsers (req, res, next) {
try {
const CONSTANTS = getConstants()
@@ -132,9 +150,14 @@ async function getUsers (req, res, next) {
}
/**
- * Get the details of a single user for the specified username
- * Called by GET /api/registry/org/{shortname}/user/{username}, GET /api/org/{shortname}/user/{username}
- **/
+ * Get the details of a single user for the specified username.
+ * Called by GET /api/org/{shortname}/user/{username}
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
+ */
async function getUser (req, res, next) {
try {
const shortName = req.ctx.org
@@ -164,7 +187,7 @@ async function getUser (req, res, next) {
return res.status(404).json(error.userDne(username))
}
- const rawResult = result.toObject()
+ const rawResult = result
delete rawResult._id
delete rawResult.__v
@@ -178,9 +201,14 @@ async function getUser (req, res, next) {
}
/**
- * Get details on ID quota for an org with the specified org shortname
+ * Get details on ID quota for an org with the specified org shortname.
* Called by GET /api/registry/org/{shortname}/id_quota, GET /api/org/{shortname}/id_quota
- **/
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
+ */
async function getOrgIdQuota (req, res, next) {
try {
const session = await mongoose.startSession()
@@ -219,10 +247,15 @@ async function getOrgIdQuota (req, res, next) {
}
/**
- * Creates a new org only if the org doesn't exist for the specified shortname.
- * If the org exists, we do not update the org.
- * Called by POST /api/org/
- **/
+ * Creates a new org only if the org doesn't exist for the specified shortname.
+ * If the org exists, we do not update the org.
+ * Called by POST /api/org/
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
+ */
async function createOrg (req, res, next) {
try {
const session = await mongoose.startSession()
@@ -290,10 +323,15 @@ async function createOrg (req, res, next) {
}
/**
- * Updates an org only if the org exist for the specified shortname.
- * If no org exists, we do not create the org.
- * Called by PUT /api/org/{shortname}
- **/
+ * Updates an org only if the org exist for the specified shortname.
+ * If no org exists, we do not create the org.
+ * Called by PUT /api/org/{shortname}
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
+ */
async function updateOrg (req, res, next) {
const shortNameUrlParameter = req.ctx.params.shortname
const orgRepository = req.ctx.repositories.getBaseOrgRepository()
@@ -360,10 +398,14 @@ 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
+ * Creates a user only if the org exists and the user does not exist for the specified shortname and username.
* Called by POST /api/registry/org/{shortname}/user, POST /api/org/{shortname}/user
- **/
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
+ */
async function createUser (req, res, next) {
const session = await mongoose.startSession()
try {
@@ -460,10 +502,15 @@ 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}, PUT /org/{shortname}/user/{username}
- **/
+ * 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}, PUT /org/{shortname}/user/{username}
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
+ */
async function updateUser (req, res, next) {
const session = await mongoose.startSession()
@@ -621,6 +668,11 @@ async function updateUser (req, res, next) {
/**
* Resets API secret for specified user.
* Called by PUT /org/{shortname}/user/{username}/reset_secret, PUT /registry/org/{shortname}/user/{username}/reset_secret
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object
+ * @param {Function} next - The next middleware function
+ * @returns {Promise}
*/
async function resetSecret (req, res, next) {
try {
diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js
index 9341589db..6159a556c 100644
--- a/src/controller/org.controller/org.middleware.js
+++ b/src/controller/org.controller/org.middleware.js
@@ -31,7 +31,7 @@ function validateCreateOrgParameters () {
// soft_quota,
// Not allowed
// users, contact_info.admins, in_use, created, last_updated
- const orgOptions = ['Top Level Root', 'Root', 'CNA', 'CNA-LR', 'Secretariat', 'Board', 'AWG', 'TWG', 'SPWG', 'Bulk Download', 'ADP']
+ const orgOptions = ['CNA', 'Secretariat', 'Bulk Download', 'ADP']
validations = [
body(['short_name']).isString()
.trim()
@@ -333,6 +333,8 @@ const QUERY_PARAMETERS = {
}
function parsePutParams (req, res, next) {
+ console.log('DEBUG: parsePutParams req.body:', JSON.stringify(req.body))
+ req.body = utils.deepRemoveEmpty(req.body)
utils.reqCtxMapping(req, 'body', [])
// Extract all possible query parameters
const allQueryParams = [
@@ -346,6 +348,7 @@ function parsePutParams (req, res, next) {
}
function parsePostParams (req, res, next) {
+ req.body = utils.deepRemoveEmpty(req.body)
utils.reqCtxMapping(req, 'body', [])
utils.reqCtxMapping(req, 'query', [])
utils.reqCtxMapping(req, 'params', ['shortname', 'username', 'identifier'])
diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js
index 36dad9aa7..efd75791b 100644
--- a/src/controller/registry-org.controller/index.js
+++ b/src/controller/registry-org.controller/index.js
@@ -212,6 +212,7 @@ router.post('/registryOrg',
}
*/
mw.useRegistry(),
+ mw.onlySecretariat,
mw.validateUser,
parseError,
parsePostParams,
@@ -299,6 +300,7 @@ 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 be3880ee3..ba0ca21c5 100644
--- a/src/controller/registry-org.controller/registry-org.controller.js
+++ b/src/controller/registry-org.controller/registry-org.controller.js
@@ -22,6 +22,8 @@ async function getAllOrgs (req, res, next) {
try {
const session = await mongoose.startSession()
const repo = req.ctx.repositories.getBaseOrgRepository()
+ const conversationRepo = req.ctx.repositories.getConversationRepository()
+ const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session })
const CONSTANTS = getConstants()
let returnValue
@@ -37,6 +39,11 @@ async function getAllOrgs (req, res, next) {
try {
returnValue = await repo.getAllOrgs({ ...options, session })
+ // fetch conversations
+ for (let i = 0; i < returnValue.organizations.length; i++) {
+ const conversation = await conversationRepo.getAllByTargetUUID(returnValue.organizations[i].UUID, isSecretariat, { session })
+ returnValue.organizations[i].conversation = conversation?.length ? conversation : undefined
+ }
} finally {
await session.endSession()
}
@@ -64,6 +71,7 @@ async function getOrg (req, res, next) {
try {
const session = await mongoose.startSession()
const repo = req.ctx.repositories.getBaseOrgRepository()
+ const conversationRepo = req.ctx.repositories.getConversationRepository()
// User passed in parameter to filter for
const identifier = req.ctx.params.identifier
const requesterOrgShortName = req.ctx.org
@@ -82,8 +90,18 @@ async function getOrg (req, res, next) {
}
returnValue = await repo.getOrg(identifier, identifierIsUUID, { session })
+
+ if (returnValue) {
+ // fetch conversation
+ const conversation = await conversationRepo.getAllByTargetUUID(returnValue.UUID, isSecretariat, { session })
+ returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'UUID', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid', 'visibility'])) : undefined
+ }
} 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()
@@ -116,11 +134,8 @@ 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
@@ -134,15 +149,10 @@ async function createOrg (req, res, next) {
if (!result.isValid) {
logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' }))
await session.abortTransaction()
-
- // TODO: Investigate this, right now we are accepting either a one one-dimensional array of strings or just a string.
- // However, we are taking the "highest" authority
- //
- //
- // 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({ message: 'Parameters were invalid', errors: result.errors })
+ if (!Array.isArray(body?.authority) || body?.authority.some(item => typeof item !== 'string')) {
+ 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({ error: 'BAD_INPUT', message: 'Parameters were invalid', errors: result.errors })
}
// Check for duplicate short_name
@@ -155,6 +165,8 @@ async function createOrg (req, res, next) {
return res.status(400).json(error.orgExists(body?.short_name))
}
+ const userRepo = req.ctx.repositories.getBaseUserRepository()
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
// Create the org – repo.createOrg will handle field mapping
createdOrg = await repo.createOrg(body, { session, upsert: true }, false, requestingUserUUID, isSecretariat)
@@ -245,16 +257,13 @@ async function updateOrg (req, res, next) {
if (!org) {
// resolve edge case
const reviewRepo = req.ctx.repositories.getReviewObjectRepository()
- const reviewOrg = await reviewRepo.getOrgReviewObjectStandaloneByRequestedOrgShortname(shortName, { session })
+ const reviewOrg = await reviewRepo.getOrgReviewObjectByOrgShortname(shortName, isSecretariat, { 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' })
}
@@ -282,10 +291,41 @@ async function updateOrg (req, res, next) {
return res.status(400).json(error.duplicateShortname(body?.short_name))
}
+ // Handle secretariat "stomping" of pending review objects
+ if (isSecretariat) {
+ const reviewRepo = req.ctx.repositories.getReviewObjectRepository()
+ const pendingReview = await reviewRepo.getOrgReviewObjectByOrgShortname(shortName, isSecretariat, { session })
+
+ if (pendingReview) {
+ const pendingReviewData = pendingReview.new_review_data
+
+ // Merge to get full expected state from pending review vs incoming
+ const pendingFullState = _.merge({}, org.toObject(), pendingReviewData)
+ const incomingFullState = _.merge({}, org.toObject(), body)
+
+ // Clean for comparison (remove metadata)
+ const cleanPending = _.omit(pendingFullState, ['_id', '__v', '__t', 'createdAt', 'updatedAt', 'created', 'last_updated'])
+ const cleanIncoming = _.omit(incomingFullState, ['_id', '__v', '__t', 'createdAt', 'updatedAt', 'created', 'last_updated'])
+
+ // Compare and set status accordingly
+ if (_.isEqual(cleanPending, cleanIncoming)) {
+ await reviewRepo.approveReviewOrgObject(pendingReview.uuid, { session })
+ } else {
+ await reviewRepo.rejectReviewOrgObject(pendingReview.uuid, { 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()
+ session.startTransaction()
+ // Checking for existing Conversations
+ const existingConversations = await conversationRepo.getAllByTargetUUID(updatedOrg.UUID, isSecretariat, { session }) || []
+ updatedOrg.conversation = existingConversations.map(c => _.omit(c, ['__v', '_id', 'previous_conversation_uuid', 'next_conversation_uuid']))
+
await session.commitTransaction()
} catch (updateErr) {
await session.abortTransaction()
@@ -392,7 +432,7 @@ async function deleteOrg (req, res, next) {
* @param {object} req - The Express request object, containing the organization shortname in `req.ctx.params.shortname`.
* @param {object} res - The Express response object.
* @param {function} next - The next middleware function.
- * @returns {Promise} - A promise that resolves when the response is sent.
+ * @returns {Promise} - A promise that resolves when the response is sent. Response body includes 'role' field for admins.
* @description All registered users can access this endpoint. Regular, CNA & Admin Users can retrieve information about users in the same organization.
* Secretariat can retrieve all user information for any organization.
* Called by GET /api/registryOrg/:shortname/users
@@ -415,7 +455,7 @@ async function getUsers (req, res, next) {
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
const userRepo = req.ctx.repositories.getBaseUserRepository()
const orgUUID = await orgRepo.getOrgUUID(orgShortName)
- const isSecretariat = await orgRepo.isSecretariat(shortName)
+ const isSecretariat = await orgRepo.isSecretariatByShortName(shortName)
if (!orgUUID) {
logger.info({ uuid: req.ctx.uuid, message: orgShortName + ' organization does not exist.' })
@@ -427,7 +467,14 @@ async function getUsers (req, res, next) {
return res.status(403).json(error.notSameOrgOrSecretariat())
}
- const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, false)
+ // This should always return Registry typed
+ const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, true)
+
+ // Hydrate the role field
+ const org = await orgRepo.findOneByShortName(orgShortName)
+ payload.users.forEach(user => {
+ user.role = org.admins.includes(user.UUID) ? 'ADMIN' : user.role // Default to existing role if not admin
+ })
logger.info({ uuid: req.ctx.uuid, message: `The users of ${orgShortName} organization were sent to the user.` })
return res.status(200).json(payload)
diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js
index 325c9d107..b6ac49ca4 100644
--- a/src/controller/registry-org.controller/registry-org.middleware.js
+++ b/src/controller/registry-org.controller/registry-org.middleware.js
@@ -5,6 +5,7 @@ const errors = require('./error')
const error = new errors.RegistryOrgControllerError()
function parsePostParams (req, res, next) {
+ req.body = utils.deepRemoveEmpty(req.body)
utils.reqCtxMapping(req, 'body', [])
utils.reqCtxMapping(req, 'params', ['identifier', 'shortname'])
utils.reqCtxMapping(req, 'query', [
diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js
index db97b684b..872c6fe11 100644
--- a/src/controller/registry-user.controller/index.js
+++ b/src/controller/registry-user.controller/index.js
@@ -9,7 +9,7 @@ const CONSTANTS = getConstants()
router.get('/registryUser',
/*
- #swagger.tags = ['Registry User']
+ #swagger.tags = ['Secretariat Only Utility Endpoints']
#swagger.operationId = 'getAllRegistryUsers'
#swagger.ignore = true
#swagger.summary = "Retrieves information about all registry users (accessible to Secretariat only)"
@@ -76,7 +76,7 @@ router.get('/registryUser',
router.get('/registryUser/:identifier',
/*
- #swagger.tags = ['Registry User']
+ #swagger.tags = ['Secretariat Only Utility Endpoints']
#swagger.operationId = 'getSingleRegistryUser'
#swagger.ignore = true
#swagger.summary = "Retrieves information about a specific registry user"
@@ -147,7 +147,7 @@ router.get('/registryUser/:identifier',
router.post('/registryUser/:shortname',
/*
- #swagger.tags = ['Registry User']
+ #swagger.tags = ['Secretariat Only Utility Endpoints']
#swagger.operationId = 'createRegistryUser'
#swagger.ignore = true
#swagger.summary = "Creates a new registry user (accessible to Secretariat only)"
@@ -218,7 +218,7 @@ router.post('/registryUser/:shortname',
router.put('/registryUser/:identifier',
/*
- #swagger.tags = ['Registry User']
+ #swagger.tags = ['Secretariat Only Utility Endpoints']
#swagger.operationId = 'updateRegistryUser'
#swagger.ignore = true
#swagger.summary = "Updates an existing registry user (accessible to Secretariat only)"
@@ -307,7 +307,7 @@ router.put('/registryUser/:identifier',
router.delete(
'/registryUser/:identifier',
/*
- #swagger.tags = ['Registry User']
+ #swagger.tags = ['Secretariat Only Utility Endpoints']
#swagger.operationId = 'deleteRegistryUser'
#swagger.ignore = true
#swagger.summary = "Deletes an existing registry user (accessible to Secretariat only)"
diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js
index cf5825a5f..fdd11eaf7 100644
--- a/src/controller/registry-user.controller/registry-user.controller.js
+++ b/src/controller/registry-user.controller/registry-user.controller.js
@@ -3,7 +3,21 @@ const logger = require('../../middleware/logger')
const { getConstants } = require('../../constants')
const errors = require('../user.controller/error')
const error = new errors.UserControllerError()
+const validateUUID = require('uuid').validate
+const _ = require('lodash')
+/**
+ * Retrieves information about all registry users.
+ *
+ * @async
+ * @function getAllUsers
+ * @param {object} req - The Express request object.
+ * @param {object} res - The Express response object.
+ * @param {function} next - The next middleware function.
+ * @returns {Promise} - A promise that resolves when the response is sent. Response body includes 'role' field for admins.
+ * @description This endpoint is accessible to Secretariat only. It retrieves a list of all registry users.
+ * Called by GET /api/registryUser
+ */
async function getAllUsers (req, res, next) {
try {
const CONSTANTS = getConstants()
@@ -23,6 +37,30 @@ async function getAllUsers (req, res, next) {
try {
returnValue = await repo.getAllUsers(options)
+ // Hydrate roles
+ const orgRepo = req.ctx.repositories.getBaseOrgRepository()
+ const distinctOrgUUIDs = [...new Set(returnValue.users.map(u => u.org_UUID))]
+
+ // Fetch all relevant orgs in one go (or in parallel) if possible, but map is easy for now
+ // Since we don't have a "getManyOrgsByUUID", we might need to do it one by one or improve repository
+ // For now, let's iterate and fetch. It's not optimal but safe given repo limitations.
+ // Optimization: We can build a map of orgUUID -> orgObject
+ const orgMap = {}
+ for (const uuid of distinctOrgUUIDs) {
+ // We need the org content to get admins
+ const org = await orgRepo.findOneByUUID(uuid)
+ if (org) {
+ orgMap[uuid] = org
+ }
+ }
+
+ returnValue.users.forEach(user => {
+ const org = orgMap[user.org_UUID]
+ if (org && org.admins && org.admins.includes(user.UUID)) {
+ user.role = 'ADMIN'
+ }
+ // If not admin, leave as is (undefined or empty or whatever it was)
+ })
} finally {
await session.endSession()
}
@@ -34,17 +72,71 @@ async function getAllUsers (req, res, next) {
}
}
+/**
+ * Retrieves information about a specific registry user.
+ *
+ * @async
+ * @function getUser
+ * @param {object} req - The Express request object.
+ * @param {object} res - The Express response object.
+ * @param {function} next - The next middleware function.
+ * @returns {Promise} - A promise that resolves when the response is sent. Response body includes 'role' field for admins.
+ * @description All authenticated users can access this endpoint. It retrieves information about the specified registry user.
+ * Called by GET /api/registryUser/:identifier
+ */
async function getUser (req, res, next) {
+ /*
+ This function is a little bit overloaded ATM until future releases of CVE-Services
+ Currently it can be called with just an identifier (UUID) OR with the org shortname and username
+
+ We need to make sure that either way we convert to one or the other. For now, I am going shortname / username
+ */
+ // Check to see if identifier is set
+ const identifier = req.ctx.params.identifier
+
+ // if identifier is set, BUT it is a username
+ if (identifier && !validateUUID(identifier)) {
+ return res.status(400).json({ error: 'This function expects a UUID when called this way' })
+ }
+
+ let userToGetParameters = {
+ org: req.ctx.params.shortname,
+ username: req.ctx.params.username
+ }
+
+ const userRepo = req.ctx.repositories.getBaseUserRepository()
+ const repo = req.ctx.repositories.getBaseOrgRepository()
+ const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org)
+
try {
- const repo = req.ctx.repositories.getBaseUserRepository()
- const identifier = req.ctx.params.identifier
+ const result = identifier
+ ? await userRepo.getUserUUID(identifier)
+ : await userRepo.findOneByUsernameAndOrgShortname(userToGetParameters.username, userToGetParameters.org)
+
+ const org = identifier
+ ? await repo.getOrg(identifier, true)
+ : await repo.getOrg(req.ctx.params.shortname)
- 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))
+ logger.info({ uuid: req.ctx.uuid, message: identifier || userToGetParameters.username + 'user could not be found.' })
+ return res.status(404).json(error.userDne(userToGetParameters.username))
+ }
+ userToGetParameters = {
+ org: org.short_name,
+ username: result.username
+ }
+
+ if (!isSecretariat && req.ctx.org !== userToGetParameters.org) {
+ logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' })
+ return res.status(403).json(error.notSameOrgOrSecretariat())
+ }
+
+ const user = result.toObject ? result.toObject() : result
+ const userPayload = _.omit(user, ['secret', '_id', '__v'])
+ if (org.admins?.includes(userPayload.UUID)) {
+ userPayload.role = 'ADMIN'
}
- return res.status(200).json(result)
+ return res.status(200).json(userPayload)
} catch (err) {
next(err)
}
@@ -121,26 +213,132 @@ async function createUser (req, res, next) {
}
async function updateUser (req, res, next) {
+ /*
+ This function is a little bit overloaded ATM until future releases of CVE-Services
+ Currently it can be called with just an identifier (UUID) OR with the org shortname and username
+
+ We need to make sure that either way we convert to one or the other. For now, I am going shortname / username
+ */
const session = await mongoose.startSession()
- const userUUID = req.ctx.params.identifier
- const userRepo = req.ctx.repositories.getBaseUserRepository()
+ // Check to see if identifier is set
+ const identifier = req.ctx.params.identifier
+
+ // if identifier is set, BUT it is a username
+ if (identifier && !validateUUID(identifier)) {
+ return res.status(400).json({ error: 'This function expects a UUID when called this way' })
+ }
+
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
+ const userRepo = req.ctx.repositories.getBaseUserRepository()
+
const body = req.ctx.body
- let result
+ const requestingUserParameters = {
+ org: req.ctx.org,
+ username: req.ctx.user
+ }
+
+ const userToEditParameters = {
+ org: req.ctx.params.shortname,
+ username: req.ctx.params.username
+ }
+
+ const isSecretariat = await orgRepo.isSecretariatByShortName(requestingUserParameters.org, { session })
+ const isAdmin = await userRepo.isAdmin(requestingUserParameters.username, userToEditParameters.org, { session })
+
+ // TODO: This will need to be atomic at some point like revoke or grant
+ // Specific check for org_short_name (Secretariat only)
+
+ const userToEdit = identifier
+ ? await userRepo.getUserUUID(identifier)
+ : await userRepo.findOneByUsernameAndOrgShortname(userToEditParameters.username, userToEditParameters.org, { session })
+
+ const org = await orgRepo.findOneByShortName(userToEditParameters.org)
+
+ if (body.org_short_name && !isSecretariat) {
+ logger.info({ uuid: req.ctx.uuid, message: 'Only Secretariat can reassign user organization.' })
+ return res.status(403).json(error.notAllowedToChangeOrganization())
+ }
+
+ if (body.org_short_name && isSecretariat && userToEditParameters.org === org.short_name && body.org_short_name === org.short_name) {
+ logger.info({ uuid: req.ctx.uuid, message: `User ${userToEditParameters.username} is already in organization ${userToEditParameters.org}.` })
+ return res.status(403).json(error.alreadyInOrg(org.short_name, userToEditParameters.username))
+ }
+
+ if (!org) {
+ logger.info({ uuid: req.ctx.uuid, message: 'Org DNE' })
+ return res.status(404).json(error.orgDnePathParam(userToEditParameters.org))
+ }
+
+ if (!isSecretariat && !isAdmin && requestingUserParameters.org !== userToEditParameters.org) {
+ logger.info({ uuid: req.ctx.uuid, message: requestingUserParameters.org + ' user can only be updated by the user or admins of the same organization or the Secretariat.' })
+ return res.status(403).json(error.notSameOrgOrSecretariat())
+ }
+
+ if (!isSecretariat && !isAdmin) {
+ if (requestingUserParameters.username !== userToEditParameters.username) {
+ if (!userToEdit) {
+ logger.info({ uuid: req.ctx.uuid, message: 'User DNE' })
+ return res.status(404).json(error.userDne(userToEditParameters.username))
+ }
+ logger.info({ uuid: req.ctx.uuid, message: 'Not same user or secretariat' })
+ return res.status(403).json(error.notSameUserOrSecretariat())
+ }
+ }
+
+ if (!org) {
+ logger.info({ uuid: req.ctx.uuid, message: `Target organization ${userToEditParameters.org} does not exist.` })
+ return res.status(404).json(error.orgDnePathParam(userToEditParameters.org))
+ }
+
+ if (!userToEdit) {
+ logger.info({ uuid: req.ctx.uuid, message: userToEditParameters.username + ' user could not be found.' })
+ return res.status(404).json(error.userDne(userToEditParameters.username))
+ }
+
+ if (!isSecretariat) {
+ // For now, we want to make sure that no one, other than a secretariat can edit time fields
+ delete body.created
+ delete body.last_updated
+ }
+
+ let result
+ let updatedUser
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' }] })
+ // if a user is NOT an ADMIN OR SECRETARIAT they can only update their name fields
+ if (!isSecretariat && !isAdmin) {
+ const allowedFields = ['name', 'name.first', 'name.last', 'name.middle', 'name.suffix']
+
+ const restrictedUpdates = _.omit(body, allowedFields)
+ const keysToCheck = Object.keys(restrictedUpdates)
+ const originalValues = _.pick(JSON.parse(JSON.stringify(userToEdit)), keysToCheck)
+
+ if (!_.isEqual(restrictedUpdates, originalValues)) {
+ logger.info({ uuid: req.ctx.uuid, message: 'Regular users can only update their contact info.' })
+ await session.abortTransaction()
+ return res.status(400).json(error.notAllowedToChangeField())
+ }
}
+
+ result = await userRepo.validateUser(body)
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 })
+
+ // Ask repo if user already exists
+ if (body?.username && body.username !== userToEdit.username) {
+ if (await userRepo.orgHasUser(userToEditParameters.org, body.username, { session })) {
+ logger.info({ uuid: req.ctx.uuid, message: 'The username ' + body.username + ' already exists.' })
+ await session.abortTransaction()
+ return res.status(403).json(error.duplicateUsername())
+ }
+ }
+
+ updatedUser = await userRepo.updateUserFull(userToEdit.UUID, body, { session })
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -151,26 +349,20 @@ async function updateUser (req, res, next) {
const payload = {
action: 'update_registry_user',
- change: result.user_id + ' was successfully updated.',
+ change: userToEditParameters.username + ' was successfully updated.',
req_UUID: req.ctx.uuid,
- org_UUID: await orgRepo.getOrgUUID(req.ctx.org),
- user: result
+ org_UUID: org.UUID,
+ user: updatedUser
}
payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID)
logger.info(JSON.stringify(payload))
- let msgStr = ''
- if (Object.keys(req.ctx.query).length > 0) {
- msgStr = result.user_id + ' was successfully updated.'
- } else {
- msgStr = 'No updates were specified for ' + result.user_id + '.'
- }
- const responseMessage = {
- message: msgStr,
- updated: result
- }
-
- return res.status(200).json(responseMessage)
+ return res.status(200).json(
+ {
+ message: userToEditParameters.username + ' was successfully updated.',
+ updated: updatedUser
+ }
+ )
} catch (err) {
next(err)
}
@@ -205,10 +397,142 @@ async function deleteUser (req, res, next) {
}
}
+async function grantRole (req, res, next) {
+ const session = await mongoose.startSession()
+ try {
+ const orgShortName = req.ctx.params.shortname
+ const username = req.ctx.params.username
+ const role = req.ctx.body.role
+ const callingUser = req.ctx.user
+ const callingOrg = req.ctx.org
+
+ const userRepo = req.ctx.repositories.getBaseUserRepository()
+ const orgRepo = req.ctx.repositories.getBaseOrgRepository()
+
+ // Right now, we only allow users to be admin
+ if (role !== 'ADMIN') {
+ return res.status(400).json(
+ {
+ error: 'BAD_INPUT',
+ message: 'Invalid role request. Granting of this role is not supported.'
+ })
+ }
+
+ // Check if target org exists
+ const targetOrgUUID = await orgRepo.getOrgUUID(orgShortName)
+ if (!targetOrgUUID) {
+ return res.status(404).json(error.orgDnePathParam(orgShortName))
+ }
+
+ // Check if target user exists in target org
+ const targetUser = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName)
+ if (!targetUser) {
+ return res.status(404).json(error.userDne(username))
+ }
+
+ const isSecretariat = await orgRepo.isSecretariatByShortName(callingOrg)
+ const isAdmin = await userRepo.isAdmin(callingUser, callingOrg)
+
+ if (callingOrg !== orgShortName && !isSecretariat) {
+ return res.status(403).json(error.notSameOrgOrSecretariat())
+ }
+
+ if (!isSecretariat && !isAdmin) {
+ return res.status(403).json(error.notOrgAdminOrSecretariatUpdate())
+ }
+
+ try {
+ session.startTransaction()
+ await orgRepo.addAdmin(orgShortName, targetUser.UUID, { session })
+ await session.commitTransaction()
+ } catch (error) {
+ await session.abortTransaction()
+ throw error
+ } finally {
+ await session.endSession()
+ }
+
+ logger.info({ uuid: req.ctx.uuid, message: `Role ${role} granted to user ${username} in org ${orgShortName}` })
+ return res.status(200).json({ message: `Role ${role} granted to user ${username}.` })
+ } catch (err) {
+ next(err)
+ }
+}
+
+async function revokeRole (req, res, next) {
+ const session = await mongoose.startSession()
+ try {
+ const orgShortName = req.ctx.params.shortname
+ const username = req.ctx.params.username
+ const role = req.ctx.body.role
+ const callingUser = req.ctx.user
+ const callingOrg = req.ctx.org
+
+ const userRepo = req.ctx.repositories.getBaseUserRepository()
+ const orgRepo = req.ctx.repositories.getBaseOrgRepository()
+
+ // Right now, we only allow users to be admin
+ if (role !== 'ADMIN') {
+ return res.status(400).json(
+ {
+ error: 'BAD_INPUT',
+ message: 'Invalid role request. Revocation of this role is not supported.'
+ })
+ }
+
+ // Check if target org exists
+ const targetOrgUUID = await orgRepo.getOrgUUID(orgShortName)
+ if (!targetOrgUUID) {
+ return res.status(404).json(error.orgDnePathParam(orgShortName))
+ }
+
+ // Check if target user exists in target org
+ const targetUser = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName)
+ if (!targetUser) {
+ return res.status(404).json(error.userDne(username))
+ }
+
+ const isSecretariat = await orgRepo.isSecretariatByShortName(callingOrg)
+ const isAdmin = await userRepo.isAdmin(callingUser, callingOrg)
+
+ if (callingOrg !== orgShortName && !isSecretariat) {
+ return res.status(403).json(error.notSameOrgOrSecretariat())
+ }
+
+ if (!isSecretariat && !isAdmin) {
+ return res.status(403).json(error.notOrgAdminOrSecretariatUpdate())
+ }
+
+ // Prevent Self-Demotion
+ const callingUserUUID = await userRepo.getUserUUID(callingUser, callingOrg)
+ if (callingUserUUID === targetUser.UUID) {
+ return res.status(403).json({ error: 'NOT_ALLOWED_TO_SELF_DEMOTE', message: 'You cannot remove the ADMIN role from yourself.' })
+ }
+
+ try {
+ session.startTransaction()
+ await orgRepo.removeAdmin(orgShortName, targetUser.UUID, { session })
+ await session.commitTransaction()
+ } catch (error) {
+ await session.abortTransaction()
+ throw error
+ } finally {
+ await session.endSession()
+ }
+
+ logger.info({ uuid: req.ctx.uuid, message: `Role ${role} revoked from user ${username} in org ${orgShortName}` })
+ return res.status(200).json({ message: `Role ${role} revoked from user ${username}.` })
+ } catch (err) {
+ next(err)
+ }
+}
+
module.exports = {
ALL_USERS: getAllUsers,
SINGLE_USER: getUser,
CREATE_USER: createUser,
UPDATE_USER: updateUser,
- DELETE_USER: deleteUser
+ DELETE_USER: deleteUser,
+ GRANT_ROLE: grantRole,
+ REVOKE_ROLE: revokeRole
}
diff --git a/src/controller/review-object.controller/error.js b/src/controller/review-object.controller/error.js
new file mode 100644
index 000000000..36ff0b879
--- /dev/null
+++ b/src/controller/review-object.controller/error.js
@@ -0,0 +1,14 @@
+const idrErr = require('../../utils/error')
+
+class ReviewObjectControllerError extends idrErr.IDRError {
+ orgDnePathParam (shortname) {
+ const err = {}
+ err.error = 'ORG_DNE_PARAM'
+ err.message = `The '${shortname}' organization designated by the shortname path parameter does not exist.`
+ return err
+ }
+}
+
+module.exports = {
+ ReviewObjectControllerError
+}
diff --git a/src/controller/review-object.controller/index.js b/src/controller/review-object.controller/index.js
index 8c2b65a08..190db6695 100644
--- a/src/controller/review-object.controller/index.js
+++ b/src/controller/review-object.controller/index.js
@@ -1,12 +1,639 @@
const router = require('express').Router()
+const { query } = require('express-validator')
const controller = require('./review-object.controller')
const mw = require('../../middleware/middleware')
+const { parseError } = require('./review-object.middleware')
+const getConstants = require('../../constants').getConstants
+const CONSTANTS = getConstants()
-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)
+// Get review object by UUID
+router.get('/review/byUUID/:uuid',
+ /*
+ #swagger.tags = ['Review Object']
+ #swagger.operationId = 'getReviewObjectByUUID'
+ #swagger.summary = "Retrieves a review object by its UUID (accessible to Secretariat or Admin)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role or have the Admin role
"
+ #swagger.parameters['uuid'] = { description: 'The UUID of the review object' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Returns the review object',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/review/review.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ mw.onlySecretariatOrAdmin,
+ controller.getReviewObjectByUUID
+)
+
+// Get pending review object for an organization
+router.get('/review/org/:identifier',
+ /*
+ #swagger.tags = ['Review Object']
+ #swagger.operationId = 'getReviewObjectByOrgIdentifier'
+ #swagger.summary = "Retrieves the PENDING review object for an organization (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
"
+ #swagger.parameters['identifier'] = { description: 'The short name or UUID of the organization' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Returns the pending review object',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/review/review.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ mw.onlySecretariat,
+ controller.getReviewObjectByOrgIdentifier
+)
+
+// Get all review objects
+router.get('/review/orgs',
+ /*
+ #swagger.tags = ['Review Object']
+ #swagger.operationId = 'getAllReviewObjects'
+ #swagger.summary = "Retrieves all review objects (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
"
+ #swagger.parameters['page'] = {
+ in: 'query',
+ description: 'The page of results to retrieve',
+ type: 'integer'
+ }
+ #swagger.parameters['status'] = {
+ in: 'query',
+ description: 'Filter by review object status',
+ type: 'string'
+ }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Returns a list of review objects',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/review/list-reviews-response.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ mw.onlySecretariat,
+ query().custom((query) => { return mw.validateQueryParameterNames(query, ['page', 'status']) }),
+ query(['page', 'status']).custom((val) => { return mw.containsNoInvalidCharacters(val) }),
+ query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }),
+ query(['status']).optional().isString(),
+ parseError,
+ controller.getAllReviewObjects
+)
+
+// Get review history for an organization
+router.get('/review/org/:identifier/reviews',
+ /*
+ #swagger.tags = ['Review Object']
+ #swagger.operationId = 'getReviewHistoryByOrgShortNamePaginated'
+ #swagger.summary = "Retrieves the review history for an organization (accessible to Secretariat or Admin)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role or have the Admin role
"
+ #swagger.parameters['identifier'] = { description: 'The short name of the organization' }
+ #swagger.parameters['page'] = {
+ in: 'query',
+ description: 'The page of results to retrieve',
+ type: 'integer'
+ }
+ #swagger.parameters['include_conversations'] = {
+ in: 'query',
+ description: 'Whether to include conversation history',
+ type: 'boolean'
+ }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Returns the review history',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/review/list-reviews-response.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ mw.onlySecretariatOrAdmin,
+ query().custom((query) => { return mw.validateQueryParameterNames(query, ['page', 'include_conversations']) }),
+ query(['page', 'include_conversations']).custom((val) => { return mw.containsNoInvalidCharacters(val) }),
+ query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }),
+ query(['include_conversations']).optional().isBoolean().toBoolean(),
+ parseError,
+ controller.getReviewHistoryByOrgShortNamePaginated
+)
+
+// Update a review object
+router.put('/review/org/:uuid',
+ /*
+ #swagger.tags = ['Review Object']
+ #swagger.operationId = 'updateReviewObjectByReviewUUID'
+ #swagger.summary = "Updates a review object (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
"
+ #swagger.parameters['uuid'] = { description: 'The UUID of the review object' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.requestBody = {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ description: 'The updated review data'
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Returns the updated review object',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/review/review.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ mw.onlySecretariat,
+ controller.updateReviewObjectByReviewUUID
+)
+
+// Approve a review object
+router.put('/review/org/:uuid/approve',
+ /*
+ #swagger.tags = ['Review Object']
+ #swagger.operationId = 'approveReviewObject'
+ #swagger.summary = "Approves a review object and applies changes to the organization (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
"
+ #swagger.parameters['uuid'] = { description: 'The UUID of the review object' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.requestBody = {
+ required: false,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ description: 'Optional override data to apply instead of the review object data'
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Returns the updated organization',
+ content: {
+ "application/json": {
+ schema: {
+ type: 'object',
+ description: 'The updated organization object'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ mw.onlySecretariat,
+ controller.approveReviewObject
+)
+
+// Reject a review object
+router.put('/review/org/:uuid/reject',
+ /*
+ #swagger.tags = ['Review Object']
+ #swagger.operationId = 'rejectReviewObject'
+ #swagger.summary = "Rejects a review object (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
"
+ #swagger.parameters['uuid'] = { description: 'The UUID of the review object' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Returns the rejected review object',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/review/review.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ mw.onlySecretariat,
+ controller.rejectReviewObject
+)
+
+// Create a review object
+router.post('/review/org/',
+ /*
+ #swagger.tags = ['Review Object']
+ #swagger.operationId = 'createReviewObject'
+ #swagger.summary = "Creates a new review object (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
"
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.requestBody = {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ description: 'The review object data'
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Returns the created review object',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/review/review.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ 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 a54ff6d66..7795f0219 100644
--- a/src/controller/review-object.controller/review-object.controller.js
+++ b/src/controller/review-object.controller/review-object.controller.js
@@ -1,7 +1,15 @@
const validateUUID = require('uuid').validate
const mongoose = require('mongoose')
+const { getConstants } = require('../../constants')
+const errors = require('./error')
+const error = new errors.ReviewObjectControllerError()
+const _ = require('lodash')
+/**
+ * Retrieves the PENDING review object for an organization by identifier (short_name or UUID).
+ * Returns only review objects with status='pending'.
+ */
async function getReviewObjectByOrgIdentifier (req, res, next) {
const repo = req.ctx.repositories.getReviewObjectRepository()
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
@@ -19,7 +27,7 @@ async function getReviewObjectByOrgIdentifier (req, res, next) {
value = await repo.getOrgReviewObjectByOrgShortname(identifier, isSecretariat)
}
if (!value) {
- return res.status(404).json({ message: 'Review Object does not exist' })
+ return res.status(404).json({ message: 'No pending review object exists for this organization' })
}
return res.status(200).json(value)
}
@@ -35,24 +43,58 @@ async function getReviewObjectByUUID (req, res, next) {
async function getAllReviewObjects (req, res, next) {
const repo = req.ctx.repositories.getReviewObjectRepository()
- const value = await repo.getAllReviewObjects()
- return res.status(200).json(value)
+ const CONSTANTS = getConstants()
+ const status = req.query?.status || null
+
+ if (req.TEST_PAGINATOR_LIMIT) {
+ CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT
+ }
+
+ const options = CONSTANTS.PAGINATOR_OPTIONS
+ options.page = req.query?.page ? parseInt(req.query?.page) : CONSTANTS.PAGINATOR_PAGE
+
+ const response = await repo.getAllReviewObjectsPaginated(options, status)
+ return res.status(200).json(response)
}
async function approveReviewObject (req, res, next) {
- const repo = req.ctx.repositories.getReviewObjectRepository()
+ const reviewRepo = req.ctx.repositories.getReviewObjectRepository()
+ const baseOrgRepo = req.ctx.repositories.getBaseOrgRepository()
const userRepo = req.ctx.repositories.getBaseUserRepository()
const UUID = req.params.uuid
const body = req.body
const session = await mongoose.startSession()
- let value
+ let reviewObj
+ let updatedOrgObj
try {
session.startTransaction()
+
+ const reviewObject = await reviewRepo.findOneByUUID(UUID, { session })
+ if (!reviewObject) {
+ await session.abortTransaction()
+ return res.status(404).json({ message: `No review object found with UUID ${UUID}` })
+ }
+
+ const org = await baseOrgRepo.findOneByUUID(reviewObject.target_object_uuid, { session })
+ if (!org) {
+ await session.abortTransaction()
+ return res.status(404).json({ message: 'Organization not found for this review object' })
+ }
+
+ const dataToUpdate = (body && Object.keys(body).length)
+ ? _.merge({}, org.toObject(), body)
+ : reviewObject.new_review_data
+
const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
- value = await repo.approveReviewOrgObject(UUID, requestingUserUUID, { session }, body)
+ reviewObj = await reviewRepo.approveReviewOrgObject(UUID, { session })
+ await baseOrgRepo.updateOrgFull(org.short_name, dataToUpdate, { session }, false, requestingUserUUID, false, true)
+
await session.commitTransaction()
+
+ // Return the updated organization
+ updatedOrgObj = await baseOrgRepo.findOneByUUID(reviewObject.target_object_uuid)
} catch (updateErr) {
await session.abortTransaction()
throw updateErr
@@ -60,10 +102,11 @@ async function approveReviewObject (req, res, next) {
await session.endSession()
}
- if (!value) {
+ if (!reviewObj) {
return res.status(404).json({ message: `No review object found with UUID ${UUID}` })
}
- return res.status(200).json(value)
+
+ return res.status(200).json(updatedOrgObj ? updatedOrgObj.toObject() : null)
}
async function updateReviewObjectByReviewUUID (req, res, next) {
@@ -96,11 +139,70 @@ async function createReviewObject (req, res, next) {
}
return res.status(200).json(value)
}
+
+/**
+ * Retrieves the review history for an organization.
+ */
+async function getReviewHistoryByOrgShortNamePaginated (req, res, next) {
+ const reviewRepo = req.ctx.repositories.getReviewObjectRepository()
+ const orgRepo = req.ctx.repositories.getBaseOrgRepository()
+ const orgShortName = req.params.identifier
+ const includeConversations = req.query?.include_conversations
+ const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org)
+ const CONSTANTS = getConstants()
+
+ const orgExists = await orgRepo.orgExists(orgShortName)
+ if (!orgExists) {
+ return res.status(404).json(error.orgDnePathParam(orgShortName))
+ }
+
+ if (req.TEST_PAGINATOR_LIMIT) {
+ CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT
+ }
+
+ const options = CONSTANTS.PAGINATOR_OPTIONS
+ options.page = req.query?.page ? parseInt(req.query?.page) : CONSTANTS.PAGINATOR_PAGE
+
+ const response = await reviewRepo.getReviewHistoryByOrgShortNamePaginated(
+ orgShortName,
+ options,
+ includeConversations,
+ isSecretariat
+ )
+ return res.status(200).json(response)
+}
+
+async function rejectReviewObject (req, res, next) {
+ const repo = req.ctx.repositories.getReviewObjectRepository()
+ const UUID = req.params.uuid
+ const session = await mongoose.startSession()
+ let value
+
+ try {
+ session.startTransaction()
+
+ value = await repo.rejectReviewOrgObject(UUID, { session })
+ await session.commitTransaction()
+ } catch (rejectErr) {
+ await session.abortTransaction()
+ throw rejectErr
+ } 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)
+}
+
module.exports = {
getReviewObjectByOrgIdentifier,
getReviewObjectByUUID,
getAllReviewObjects,
updateReviewObjectByReviewUUID,
createReviewObject,
- approveReviewObject
+ approveReviewObject,
+ getReviewHistoryByOrgShortNamePaginated,
+ rejectReviewObject
}
diff --git a/src/controller/review-object.controller/review-object.middleware.js b/src/controller/review-object.controller/review-object.middleware.js
new file mode 100644
index 000000000..5eecc9d74
--- /dev/null
+++ b/src/controller/review-object.controller/review-object.middleware.js
@@ -0,0 +1,15 @@
+const { validationResult } = require('express-validator')
+
+function parseError (req, res, next) {
+ const err = validationResult(req).formatWith(({ location, msg, param, value, nestedErrors }) => {
+ return { msg: msg, param: param, location: location }
+ })
+ if (!err.isEmpty()) {
+ return res.status(400).json({ message: 'Bad Request', details: err.array() })
+ }
+ next()
+}
+
+module.exports = {
+ parseError
+}
diff --git a/src/controller/user.controller/error.js b/src/controller/user.controller/error.js
index 2fb7b129c..11ab679b2 100644
--- a/src/controller/user.controller/error.js
+++ b/src/controller/user.controller/error.js
@@ -1,7 +1,69 @@
const idrErr = require('../../utils/error')
class UserControllerError extends idrErr.IDRError {
+ alreadyInOrg (shortname, username) { // org
+ const err = {}
+ err.error = 'USER_ALREADY_IN_ORG'
+ err.message = `The user could not be updated because the user '${username}' already belongs to the '${shortname}' organization.`
+ return err
+ }
+ notAllowedToChangeOrganization () {
+ const err = {}
+ err.error = 'NOT_ALLOWED_TO_CHANGE_ORGANIZATION'
+ err.message = 'Only the Secretariat can change the organization for a user.'
+ return err
+ }
+
+ notAllowedToChangeField () {
+ // Welcome to the future
+ return {
+ error: 'NOT_ALLOWED_TO_CHANGE_FIELD',
+ message: 'Regular users can only update their contact info'
+ }
+ }
+
+ orgDnePathParam (shortname) { // org
+ const err = {}
+ err.error = 'ORG_DNE_PARAM'
+ err.message = `The '${shortname}' organization designated by the shortname path parameter does not exist.`
+ return err
+ }
+
+ userDne (username) { // org
+ const err = {}
+ err.error = 'USER_DNE'
+ err.message = `The user '${username}' designated by the username parameter does not exist.`
+ return err
+ }
+
+ duplicateUsername () { // org
+ const err = {}
+ err.error = 'DUPLICATE_USERNAME'
+ err.message = 'The username you have chosen already exists.'
+ return err
+ }
+
+ notSameOrgOrSecretariat () { // org
+ const err = {}
+ err.error = 'NOT_SAME_ORG_OR_SECRETARIAT'
+ err.message = 'This information can only be viewed by the users of the same organization or the Secretariat.'
+ return err
+ }
+
+ notOrgAdminOrSecretariatUpdate () {
+ const err = {}
+ err.error = 'NOT_ORG_ADMIN_OR_SECRETARIAT_UPDATE'
+ err.message = 'Contact your org Admin to update fields other than your name.'
+ return err
+ }
+
+ notSameUserOrSecretariat () { // super
+ const err = {}
+ err.error = 'NOT_SAME_USER_OR_SECRETARIAT'
+ err.message = 'This information can only be viewed or modified by the Secretariat, an Org Admin or if the requester is the user.'
+ return err
+ }
}
module.exports = {
diff --git a/src/controller/user.controller/index.js b/src/controller/user.controller/index.js
index 64f40e89b..c7d4a73d8 100644
--- a/src/controller/user.controller/index.js
+++ b/src/controller/user.controller/index.js
@@ -3,6 +3,7 @@ const router = express.Router()
const mw = require('../../middleware/middleware')
const { query, param } = require('express-validator')
const controller = require('./user.controller')
+const registryUserController = require('../registry-user.controller/registry-user.controller.js')
const { parseGetParams, parseError } = require('./user.middleware')
// Only God and Javascript know why its saying it is not used when it is.....
// eslint-disable-next-line no-unused-vars
@@ -86,7 +87,7 @@ router.get('/registry/users',
query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }),
parseError,
parseGetParams,
- controller.ALL_USERS
+ registryUserController.ALL_USERS
)
router.get('/users',
diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json
index 7ce5aa663..c09805741 100644
--- a/src/middleware/schemas/BaseOrg.json
+++ b/src/middleware/schemas/BaseOrg.json
@@ -34,7 +34,7 @@
"authority": {
"description": "The authority (role) of this organization within the CVE program",
"type": "string",
- "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP", "TLROOT", "ROOT"]
+ "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"]
},
"discriminator": {
"description": "Discriminator key used by Mongoose for type inheritance",
diff --git a/src/model/conversation.js b/src/model/conversation.js
index 1994126a7..ef7c2767b 100644
--- a/src/model/conversation.js
+++ b/src/model/conversation.js
@@ -5,6 +5,8 @@ const MongoPaging = require('mongo-cursor-pagination')
const schema = {
UUID: String,
target_uuid: String,
+ previous_conversation_uuid: String,
+ next_conversation_uuid: String,
author_id: String,
author_name: String,
author_role: String,
@@ -16,6 +18,8 @@ const schema = {
const ConversationSchema = new mongoose.Schema(schema, { collection: 'Conversation', timestamps: { createdAt: 'posted_at', updatedAt: 'last_updated' } })
ConversationSchema.index({ target_uuid: 1 })
+ConversationSchema.index({ previous_conversation_uuid: 1 })
+ConversationSchema.index({ next_conversation_uuid: 1 })
ConversationSchema.index({ author_id: 1 })
ConversationSchema.index({ posted_at: 1 })
diff --git a/src/model/reviewobject.js b/src/model/reviewobject.js
index a5436cd2c..52bdea788 100644
--- a/src/model/reviewobject.js
+++ b/src/model/reviewobject.js
@@ -15,5 +15,8 @@ ReviewOrgSchema.plugin(aggregatePaginate)
// Cursor pagination
ReviewOrgSchema.plugin(MongoPaging.mongoosePlugin)
+
+ReviewOrgSchema.index({ target_object_uuid: 1, status: 1, created: -1 })
+
const ReviewObject = mongoose.model('ReviewObject', ReviewOrgSchema)
module.exports = ReviewObject
diff --git a/src/repositories/auditRepository.js b/src/repositories/auditRepository.js
index 1beabdbca..f6f23ec44 100644
--- a/src/repositories/auditRepository.js
+++ b/src/repositories/auditRepository.js
@@ -27,8 +27,7 @@ class AuditRepository extends BaseRepository {
try {
// Try to find existing document
- let audit = await this.findOneByTargetUUID(targetUUID, options)
-
+ let audit = await this.findOneByTargetUUID(targetUUID, { options })
if (!audit) {
// Create new document if doesn't exist
// Assuming 'uuid' is available for generating a new UUID
@@ -42,8 +41,8 @@ class AuditRepository extends BaseRepository {
audit.history.push(historyEntry)
}
- const result = await audit.save(options)
- return result.toObject()
+ await audit.save({ options })
+ return audit.toObject()
} catch (error) {
throw new Error('Failed to save audit history entry.')
}
diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js
index 8f8056e17..17eadc711 100644
--- a/src/repositories/baseOrgRepository.js
+++ b/src/repositories/baseOrgRepository.js
@@ -12,6 +12,13 @@ const AuditRepository = require('./auditRepository')
const ConversationRepository = require('./conversationRepository')
const getConstants = require('../constants').getConstants
+const skipNulls = (objValue, srcValue) => {
+ if (_.isArray(objValue)) {
+ return srcValue
+ }
+ return undefined
+}
+
/**
* @function setAggregateOrgObj
* @description Constructs the aggregation pipeline for legacy organization objects.
@@ -164,6 +171,44 @@ class BaseOrgRepository extends BaseRepository {
await org.save(options)
}
+ /**
+ * @async
+ * @function addAdmin
+ * @description Adds a user to an organization's admin list.
+ * @param {string} orgShortName - The short name of the organization.
+ * @param {string} userUUID - The UUID of the user to add.
+ * @param {object} [options={}] - Optional settings for the repository query.
+ * @returns {Promise}
+ */
+ async addAdmin (orgShortName, userUUID, options = {}) {
+ const org = await this.findOneByShortName(orgShortName, options)
+ if (!org.admins) {
+ org.admins = []
+ }
+ if (!org.admins.includes(userUUID)) {
+ org.admins.push(userUUID)
+ await org.save(options)
+ }
+ }
+
+ /**
+ * @async
+ * @function removeAdmin
+ * @description Removes a user from an organization's admin list.
+ * @param {string} orgShortName - The short name of the organization.
+ * @param {string} userUUID - The UUID of the user to remove.
+ * @param {object} [options={}] - Optional settings for the repository query.
+ * @returns {Promise}
+ */
+ async removeAdmin (orgShortName, userUUID, options = {}) {
+ const org = await this.findOneByShortName(orgShortName, options)
+
+ if (org.admins && org.admins.includes(userUUID)) {
+ org.admins = org.admins.filter(uuid => uuid !== userUUID)
+ await org.save(options)
+ }
+ }
+
/**
* @async
* @function getAllOrgs
@@ -319,6 +364,9 @@ class BaseOrgRepository extends BaseRepository {
// Figure out why this is not working....
// registryObjectRaw = _.omitBy(registryObjectRaw, value => _.isNil(value) || _.isEmpty(value))
+ // Call Deep remove empty
+ registryObjectRaw = deepRemoveEmpty(registryObjectRaw)
+
// 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')) {
@@ -337,6 +385,7 @@ class BaseOrgRepository extends BaseRepository {
// set to default quota if none is specified
registryObjectRaw.hard_quota = CONSTANTS.DEFAULT_ID_QUOTA
}
+
// Write
const CNAObjectToSave = new CNAOrgModel(registryObjectRaw)
if (isSecretariat) {
@@ -682,8 +731,8 @@ class BaseOrgRepository extends BaseRepository {
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)
+ // check to see if there is a PENDING review object:
+ const reviewObject = await reviewObjectRepo.getOrgReviewObjectByOrgShortname(shortName, isSecretariat, options)
const { conversation, ...incomingOrgBody } = incomingOrg
let legacyObjectRaw
let registryObjectRaw
@@ -702,28 +751,47 @@ class BaseOrgRepository extends BaseRepository {
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.
+
+ // Dealing with roles requires a bit of extra control.
+ const originalRoles = registryOrg.authority
+
if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) {
- updatedLegacyOrg = _.merge(legacyOrg, legacyObjectRaw)
- updatedRegistryOrg = _.merge(registryOrg, registryObjectRaw)
+ updatedLegacyOrg = _.mergeWith(legacyOrg, legacyObjectRaw, skipNulls)
+ updatedRegistryOrg = _.mergeWith(registryOrg, registryObjectRaw, skipNulls)
} 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 })
+ // Check if there are actual changes to joint approval fields compared to current org object (not current review)
+ // Only compare fields that are actually in the incoming data
+ const incomingJointApprovalKeys = Object.keys(_.pick(registryObjectRaw, jointApprovalFieldsRegistry))
+ const currentJointApprovalData = _.pick(registryOrg.toObject(), incomingJointApprovalKeys)
+ const incomingJointApprovalData = _.pick(registryObjectRaw, incomingJointApprovalKeys)
+ const hasJointApprovalChanges = !_.isEqual(currentJointApprovalData, incomingJointApprovalData)
+
+ if (hasJointApprovalChanges) {
+ // write the joint approval to the database
+ jointApprovalRegistry = _.merge({}, registryOrg.toObject(), registryObjectRaw)
+ if (reviewObject) {
+ await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, { options })
+ } else {
+ await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, { 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 })
+ // If no changes between org and new object but a review object exists, remove it since joint approval is no longer needed
+ if (reviewObject) {
+ await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, { options })
+ }
}
updatedRegistryOrg = _.merge(registryOrg, _.omit(registryObjectRaw, jointApprovalFieldsRegistry))
updatedLegacyOrg = _.merge(legacyOrg, _.omit(legacyObjectRaw, jointApprovalFieldsLegacy))
}
+ // handle conversation
+ const requestingUser = await userRepo.findUserByUUID(requestingUserUUID, options)
+ if (conversation) {
+ await conversationRepo.createConversation(registryOrg.UUID, conversation, requestingUser, isSecretariat, { options })
+ }
+
// ADD AUDIT ENTRY AUTOMATICALLY for the registry object before it gets saved.
if (requestingUserUUID) {
try {
@@ -735,7 +803,7 @@ class BaseOrgRepository extends BaseRepository {
registryOrg.UUID,
currentRegistryOrg.toObject(),
requestingUserUUID,
- options
+ { ...options, upsert: true }
)
}
// Get the org state before save for comparison
@@ -753,16 +821,17 @@ class BaseOrgRepository extends BaseRepository {
registryOrg.UUID,
registryOrg.toObject(),
requestingUserUUID,
- options
+ { ...options, upsert: true }
)
}
+ console.log('Audit entry created for registry object')
} catch (auditError) {
}
}
// Handle possible authority (discriminator) changes that require a different Mongoose model
let roleChange = false
- if (!_.isEqual([...registryOrg?.authority].sort(), [...updatedRegistryOrg?.authority].sort())) {
+ if (!_.isEqual([...originalRoles].sort(), [...updatedRegistryOrg?.authority].sort())) {
roleChange = true
}
diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js
index 6d1e9edff..e9074d898 100644
--- a/src/repositories/baseUserRepository.js
+++ b/src/repositories/baseUserRepository.js
@@ -130,7 +130,7 @@ class BaseUserRepository extends BaseRepository {
if (!org || !Array.isArray(org.users)) {
return null
}
-
+ // users = users.map(user => user.toObject())
const user = users.find(user => org.users.includes(user.UUID))
if (!isRegistryObject && user) {
@@ -178,7 +178,7 @@ class BaseUserRepository extends BaseRepository {
*/
async findUserByUUID (uuid, options = {}, isRegistryObject = true) {
const legacyUserRepo = new UserRepository()
- const user = await BaseUser.find({ UUID: uuid }, null, options)
+ const user = await BaseUser.findOne({ UUID: uuid }, null, options)
if (!isRegistryObject) {
return await legacyUserRepo.findOneByUUID(user.UUID) || null
}
@@ -526,10 +526,48 @@ class BaseUserRepository extends BaseRepository {
const updatedRegistryUser = _.merge(registryUser, registryObjectRaw)
try {
+ if (incomingUser.org_short_name) {
+ const baseOrgRepository = new BaseOrgRepository()
+ const currentOrgUUID = legacyUser.org_UUID
+ const currentOrg = await baseOrgRepository.findOneByUUID(currentOrgUUID)
+ const newOrg = await baseOrgRepository.findOneByShortName(incomingUser.org_short_name)
+
+ if (!newOrg) {
+ throw new Error(`Organization ${incomingUser.org_short_name} not found`)
+ }
+
+ // 1. Remove user from old org's users list
+ currentOrg.users = currentOrg.users.filter(u => u !== identifier)
+
+ // 2. Remove user from old org's admins list (if present)
+ if (currentOrg.admins && currentOrg.admins.includes(identifier)) {
+ currentOrg.admins = currentOrg.admins.filter(a => a !== identifier)
+ }
+
+ // 3. Add user to new org's users list
+ if (!newOrg.users.includes(identifier)) {
+ newOrg.users.push(identifier)
+ }
+
+ // 4. Add user to new org's admins list (if they are an admin)
+ const isAdmin = updatedRegistryUser.role === 'ADMIN' || (updatedLegacyUser.authority && updatedLegacyUser.authority.active_roles && updatedLegacyUser.authority.active_roles.includes('ADMIN'))
+
+ if (isAdmin && newOrg.admins && !newOrg.admins.includes(identifier)) {
+ newOrg.admins.push(identifier)
+ }
+
+ // 5. Update user's org_UUID
+ updatedLegacyUser.org_UUID = newOrg.UUID
+
+ // Save org changes
+ await currentOrg.save({ options })
+ await newOrg.save({ options })
+ }
+
await updatedLegacyUser.save({ options })
await updatedRegistryUser.save({ options })
} catch (error) {
- throw new Error('Failed to update user')
+ throw new Error('Failed to update user: ' + error.message)
}
if (!isRegistryObject) {
diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js
index d7e405ff0..1558d0d15 100644
--- a/src/repositories/conversationRepository.js
+++ b/src/repositories/conversationRepository.js
@@ -35,51 +35,35 @@ class ConversationRepository extends BaseRepository {
return data
}
- async getAllByTargetUUID (targetUUID, options = {}) {
+ async getAllByTargetUUID (targetUUID, isSecretariat, options = {}) {
const conversations = await ConversationModel.find({ target_uuid: targetUUID }, null, options)
- return conversations.map(convo => convo.toObject())
+ return conversations.map(convo => convo.toObject()).filter(conv => isSecretariat || conv.visibility === 'public')
}
- async createConversation (body, options = {}) {
- body.UUID = uuid.v4()
- const newConversation = new ConversationModel(body)
+ async createConversation (targetUUID, body, user, isSecretariat, options = {}) {
+ const { getUserFullName } = require('../utils/utils')
+ const newUUID = uuid.v4()
+ // Find latest message in chain for target
+ const latestConversation = await ConversationModel.findOne({ target_uuid: targetUUID, next_conversation_uuid: null }, null, options)
+ if (latestConversation) {
+ latestConversation.next_conversation_uuid = newUUID
+ await latestConversation.save({ options })
+ }
+ const conversationObj = {
+ UUID: newUUID,
+ target_uuid: targetUUID,
+ previous_conversation_uuid: latestConversation?.UUID || null,
+ next_conversation_uuid: null,
+ author_id: user.UUID,
+ author_name: getUserFullName(user),
+ author_role: isSecretariat ? 'Secretariat' : 'Partner',
+ visibility: !isSecretariat ? 'public' : (['public', 'private'].includes(body.visibility?.toLowerCase()) ? body.visibility.toLowerCase() : 'private'),
+ body: body.body
+ }
+ const newConversation = new ConversationModel(conversationObj)
const result = await newConversation.save(options)
return result.toObject()
}
-
- async updateConversation (body, UUID, options = {}) {
- const conversation = await this.findOneByUUID(UUID)
-
- // Only allow updates to message body for now
- conversation.body = body.body
-
- 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/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js
index 048038722..24819c7d7 100644
--- a/src/repositories/reviewObjectRepository.js
+++ b/src/repositories/reviewObjectRepository.js
@@ -2,10 +2,13 @@ const ReviewObjectModel = require('../model/reviewobject')
const BaseRepository = require('./baseRepository')
const BaseOrgRepository = require('./baseOrgRepository')
const uuid = require('uuid')
-const _ = require('lodash')
class ReviewObjectRepository extends BaseRepository {
- async findOneByOrgShortName (orgShortName, options = {}) {
+ constructor () {
+ super(ReviewObjectModel)
+ }
+
+ async findByOrgShortName (orgShortName, options = {}) {
const baseOrgRepository = new BaseOrgRepository()
const org = await baseOrgRepository.findOneByShortName(orgShortName)
if (!org) {
@@ -28,29 +31,53 @@ class ReviewObjectRepository extends BaseRepository {
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')
+ const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.target_object_uuid, isSecretariat, options)
+ reviewObject.conversation = conversations?.length ? conversations : undefined
}
return reviewObject || null
}
async getAllReviewObjects (options = {}) {
- const reviewObjects = await ReviewObjectModel.find({}, null, options)
+ const reviewObjects = await ReviewObjectModel.find({}, null, {
+ ...options,
+ sort: { created: -1 }
+ })
return reviewObjects || []
}
+ /**
+ * Get all review objects with pagination, optionally filtered by status
+ * @param {object} options - Pagination options (page, limit)
+ * @param {string} status - Optional status filter (e.g., 'pending', 'approved', 'rejected')
+ */
+ async getAllReviewObjectsPaginated (options = {}, status = null) {
+ const query = status ? { status } : {}
+
+ const agt = [
+ { $match: query },
+ { $sort: { created: -1 } }
+ ]
+
+ const pg = await this.aggregatePaginate(agt, options)
+ const data = { reviewObjects: 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
+ }
+
async deleteReviewObjectByUUID (UUID, options = {}) {
const result = await ReviewObjectModel.deleteOne({ uuid: UUID }, options)
return result.deletedCount
}
- async getOrgReviewObjectStandaloneByRequestedOrgShortname (requestedOrgShortName, options = {}) {
- const reviewObject = await ReviewObjectModel.findOne({ 'new_review_data.short_name': requestedOrgShortName }, null, options)
-
- return reviewObject || null
- }
-
+ /** Gets the PENDING review object associated with the organization */
async getOrgReviewObjectByOrgShortname (orgShortName, isSecretariat, options = {}) {
const baseOrgRepository = new BaseOrgRepository()
const ConversationRepository = require('./conversationRepository')
@@ -60,11 +87,21 @@ class ReviewObjectRepository extends BaseRepository {
return null
}
let reviewObject
- const reviewObjectRaw = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options)
+ const reviewObjectRaw = await ReviewObjectModel.findOne(
+ {
+ target_object_uuid: org.UUID,
+ status: 'pending'
+ },
+ null,
+ {
+ ...options,
+ sort: { created: -1 }
+ }
+ )
if (reviewObjectRaw) {
reviewObject = reviewObjectRaw.toObject()
- const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid)
- if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public')
+ const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options)
+ reviewObject.conversation = conversations?.length ? conversations : undefined
}
return reviewObject || null
@@ -74,16 +111,26 @@ class ReviewObjectRepository extends BaseRepository {
const baseOrgRepository = new BaseOrgRepository()
const ConversationRepository = require('./conversationRepository')
const conversationRepository = new ConversationRepository()
- const org = await baseOrgRepository.findOneByUUID(orgUUID)
+ const org = await baseOrgRepository.findOneByUUID(orgUUID, options)
if (!org) {
return null
}
let reviewObject
- const reviewObjectRaw = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options)
+ const reviewObjectRaw = await ReviewObjectModel.findOne(
+ {
+ target_object_uuid: org.UUID,
+ status: 'pending'
+ },
+ null,
+ {
+ ...options,
+ sort: { created: -1 }
+ }
+ )
if (reviewObjectRaw) {
reviewObject = reviewObjectRaw.toObject()
- const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid)
- if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public')
+ const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options)
+ reviewObject.conversation = conversations?.length ? conversations : undefined
}
return reviewObject || null
@@ -116,35 +163,79 @@ class ReviewObjectRepository extends BaseRepository {
return result.toObject()
}
- async approveReviewOrgObject (UUID, requestingUserUUID, options = {}, newReviewData) {
+ async approveReviewOrgObject (UUID, options = {}) {
console.log('Approving review object with UUID:', UUID)
const reviewObject = await this.findOneByUUID(UUID, options)
if (!reviewObject) {
return null
}
+ reviewObject.status = 'approved'
+ await reviewObject.save(options)
+
+ return reviewObject.toObject()
+ }
+
+ /**
+ * Get paginated review history for an organization
+ * Returns ALL reviews (pending, approved, rejected) sorted by creation date
+ */
+ async getReviewHistoryByOrgShortNamePaginated (orgShortName, options = {}, includeConversations = false, isSecretariat = false) {
const baseOrgRepository = new BaseOrgRepository()
- const org = await baseOrgRepository.findOneByUUID(reviewObject.target_object_uuid)
+ const org = await baseOrgRepository.findOneByShortName(orgShortName, options)
if (!org) {
return null
}
- // We need to trigger the org to update
- let dataToUpdate
- if (newReviewData && Object.keys(newReviewData).length) {
- dataToUpdate = _.merge(org.toObject(), newReviewData)
- } else {
- dataToUpdate = reviewObject.new_review_data
+ const agt = [
+ { $match: { target_object_uuid: org.UUID } },
+ { $sort: { created: -1 } }
+ ]
+
+ const pg = await this.aggregatePaginate(agt, options)
+ const data = { reviewObjects: 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
}
- await baseOrgRepository.updateOrgFull(org.short_name, dataToUpdate, options, false, requestingUserUUID, false, true)
- // Delete the review object after approval
- await this.deleteReviewObjectByUUID(UUID, options)
+ // Optionally attach conversations
+ if (includeConversations && pg.itemsList && pg.itemsList.length) {
+ const ConversationRepository = require('./conversationRepository')
+ const conversationRepository = new ConversationRepository()
+
+ for (const review of data.reviewObjects) {
+ const conversations = await conversationRepository.getAllByTargetUUID(review.uuid, options)
+
+ // Filter conversations based on user role
+ if (conversations && conversations.length) {
+ review.conversation = conversations.filter(conv =>
+ isSecretariat || conv.visibility === 'public'
+ )
+ } else {
+ review.conversation = []
+ }
+ }
+ }
- // Return the updated organization
- const updatedOrg = await baseOrgRepository.findOneByUUID(reviewObject.target_object_uuid, options)
- return updatedOrg ? updatedOrg.toObject() : null
+ return data
}
-}
+ async rejectReviewOrgObject (UUID, options = {}) {
+ console.log('Rejecting review object with UUID:', UUID)
+ const reviewObject = await this.findOneByUUID(UUID, options)
+ if (!reviewObject) {
+ return null
+ }
+
+ reviewObject.status = 'rejected'
+ await reviewObject.save(options)
+
+ return reviewObject.toObject()
+ }
+}
module.exports = ReviewObjectRepository
diff --git a/src/routes.config.js b/src/routes.config.js
index ae6d38a2d..1cf2fe159 100644
--- a/src/routes.config.js
+++ b/src/routes.config.js
@@ -35,6 +35,7 @@ module.exports = async function configureRoutes (app) {
app.use('/api/', CveIdController)
app.use('/api/', SystemController)
app.use('/api/', UserController)
+ // At this time, we have moved the crud operations to mirror the cve legacy endpoint just with /registry/ in them. In the future we may want these.
app.use('/api/', RegistryUserController)
app.use('/api/', RegistryOrgController)
app.use('/api/', ConversationController)
diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js
index daf089699..7fd7fa314 100644
--- a/src/scripts/migrate.js
+++ b/src/scripts/migrate.js
@@ -12,6 +12,7 @@ const { v4: uuidv4 } = require('uuid')
const fs = require('fs')
const path = require('path')
const { MongoClient } = require('mongodb')
+const _ = require('lodash')
const dbConnStr = process.env.MONGO_CONN_STRING
const rawData = fs.readFileSync(path.join(__dirname, 'CNAlist.json'))
@@ -22,6 +23,24 @@ let allUsers
let allOrgs
let mitreUUID
+function deepRemoveEmpty (obj) {
+ if (_.isArray(obj)) {
+ return obj
+ .map(v => deepRemoveEmpty(v))
+ .filter(v => {
+ return !(v === null || (_.isArray(v) && v.length === 0) || (_.isPlainObject(v) && _.isEmpty(v)))
+ })
+ } else if (_.isPlainObject(obj)) {
+ return _.transform(obj, (result, value, key) => {
+ const cleaned = deepRemoveEmpty(value)
+ if (!(cleaned === null || (_.isArray(cleaned) && cleaned.length === 0) || (_.isPlainObject(cleaned) && _.isEmpty(cleaned)))) {
+ result[key] = cleaned
+ }
+ })
+ }
+ return obj
+}
+
async function run () {
const dbClient = new MongoClient(dbConnStr)
try {
@@ -102,8 +121,9 @@ async function addCVEBoard (db) {
last_updated: null
}
}
-
- await trgOrgCol.updateOne(trgQuery, updateDoc, options)
+ const test = deepRemoveEmpty(updateDoc)
+ console.log(test)
+ await trgOrgCol.updateOne(trgQuery, test, options)
}
async function orgHelper (db) {
@@ -206,7 +226,8 @@ async function orgHelper (db) {
last_updated: doc.time.modified
}
}
- await trgOrgCol.updateOne(trgQuery, updateDoc, options)
+ const test = deepRemoveEmpty(updateDoc)
+ await trgOrgCol.updateOne(trgQuery, test, options)
}
}
diff --git a/src/swagger.js b/src/swagger.js
index 7e217f9db..9559a608b 100644
--- a/src/swagger.js
+++ b/src/swagger.js
@@ -7,7 +7,9 @@ const endpointsFiles = [
'src/controller/user.controller/index.js',
'src/controller/system.controller/index.js',
'src/controller/registry-org.controller/index.js',
- 'src/controller/registry-user.controller/index.js'
+ 'src/controller/registry-user.controller/index.js',
+ 'src/controller/conversation.controller/index.js',
+ 'src/controller/review-object.controller/index.js'
]
const publishedCVERecord = require('../schemas/cve/published-cve-example.json')
const rejectedCVERecord = require('../schemas/cve/rejected-cve-example.json')
diff --git a/src/utils/utils.js b/src/utils/utils.js
index 2d8998c66..3ef47e2a9 100644
--- a/src/utils/utils.js
+++ b/src/utils/utils.js
@@ -48,6 +48,14 @@ async function getUserUUID (userIdentifier, orgUUID, useRegistry = false, option
}
}
+function getUserFullName (user) {
+ if (!user.name) return 'Unknown User'
+ if (!user.name.first && !user.name.last) return 'Unknown User'
+ else if (!user.name.first) return `Unknown ${user.name.last}`
+ else if (!user.name.last) return `${user.name.first} Unknown`
+ else return `${user.name.first} ${user.name.last}`
+}
+
async function isSecretariat (shortName, useRegistry = false, options = {}) {
let result = false
let orgUUID = null
@@ -290,10 +298,10 @@ function deepRemoveEmpty (obj) {
if (_.isObject(value) && !_.isArray(value) && !_.isDate(value)) {
clean(value)
}
-
// 2. After recursion, check if the key's value is an empty object or array.
// This will catch both initially empty fields and nested objects that became empty.
if (
+ value === null ||
(_.isObject(value) && !_.isDate(value) && _.isEmpty(value)) ||
(_.isArray(value) && _.isEmpty(value))
) {
@@ -316,9 +324,9 @@ module.exports = {
isEnrichedContainer,
getOrgUUID,
getUserUUID,
+ getUserFullName,
reqCtxMapping,
booleanIsTrue,
toDate,
convertDatesToISO
-
}
diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js
index 7be02fda4..5d219cab3 100644
--- a/test/integration-tests/audit/registryOrgCreatesAuditTest.js
+++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js
@@ -109,8 +109,13 @@ describe('Create and Update Audit Collection with Org Endpoints', () => {
// Now update with same values
const updateResAgain = await chai.request(app)
- .put(`/api/registry/org/${org.shortName}?name=${org.longName}`)
+ .put(`/api/registry/org/${org.shortName}`)
.set(secretariatHeaders)
+ .send({
+ short_name: org.shortName,
+ hard_quota: 1500,
+ long_name: org.longName
+ })
expect(updateResAgain).to.have.status(200)
expect(updateResAgain.body.updated.long_name).to.equal(org.longName)
@@ -130,8 +135,13 @@ describe('Create and Update Audit Collection with Org Endpoints', () => {
// Update org name
const updateRes = await chai.request(app)
- .put(`/api/registry/org/${testOrg.shortName}?id_quota=100`)
+ .put(`/api/registry/org/${testOrg.shortName}`)
.set(secretariatHeaders)
+ .send({
+ long_name: testOrg.longName,
+ short_name: testOrg.shortName,
+ hard_quota: 100
+ })
expect(updateRes).to.have.status(200)
@@ -156,18 +166,33 @@ describe('Create and Update Audit Collection with Org Endpoints', () => {
})
// Make sequential updates
const updatedRes1 = await chai.request(app)
- .put(`/api/registry/org/${testOrg.shortName}?id_quota=2000`)
+ .put(`/api/registry/org/${testOrg.shortName}`)
.set(secretariatHeaders)
+ .send({
+ short_name: testOrg.shortName,
+ long_name: testOrg.longName,
+ hard_quota: 2000
+ })
expect(updatedRes1).to.have.status(200)
const updatedRes2 = await chai.request(app)
- .put(`/api/registry/org/${testOrg.shortName}?id_quota=3000`)
+ .put(`/api/registry/org/${testOrg.shortName}`)
.set(secretariatHeaders)
+ .send({
+ short_name: testOrg.shortName,
+ long_name: testOrg.longName,
+ hard_quota: 3000
+ })
expect(updatedRes2).to.have.status(200)
const updatedRes3 = await chai.request(app)
- .put(`/api/registry/org/${testOrg.shortName}?id_quota=4000`)
+ .put(`/api/registry/org/${testOrg.shortName}`)
.set(secretariatHeaders)
+ .send({
+ short_name: testOrg.shortName,
+ long_name: testOrg.longName,
+ hard_quota: 4000
+ })
expect(updatedRes3).to.have.status(200)
// Check audit history
const auditRes = await chai.request(app)
@@ -203,8 +228,14 @@ describe('Create and Update Audit Collection with Org Endpoints', () => {
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`)
+ .put(`/api/registry/org/${testOrg.shortName}`)
.set(secretariatHeaders)
+ .send({
+ short_name: testOrg.shortName,
+ long_name: testOrg.longName,
+ authority: ['CNA'],
+ hard_quota: 2500
+ })
expect(updateRes).to.have.status(200)
// Check audit history
const auditResCreation = await chai.request(app)
diff --git a/test/integration-tests/conversation/conversationTest.js b/test/integration-tests/conversation/conversationTest.js
index 108c88960..802199ff2 100644
--- a/test/integration-tests/conversation/conversationTest.js
+++ b/test/integration-tests/conversation/conversationTest.js
@@ -9,22 +9,33 @@ const app = require('../../../src/index.js')
describe('Testing Conversation endpoints', () => {
let orgUUID
- let conversationUUID
+ let secUserUUID
+ let rootConvoUUID
before(async () => {
await chai
.request(app)
- .get('/api/org/win_5')
+ .get('/api/registry/org/win_5')
.set(constants.headers)
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
orgUUID = res.body.UUID
})
+
+ await chai
+ .request(app)
+ .get('/api/registry/org/mitre/user/test_secretariat_0@mitre.org')
+ .set(constants.headers)
+ .then((res, err) => {
+ expect(err).to.be.undefined
+ expect(res).to.have.status(200)
+ secUserUUID = res.body.UUID
+ })
})
context('Positive Tests', () => {
- it('Should create a conversation', async () => {
+ it('Should create a public conversation as Secretariat', async () => {
const conversation = {
visibility: 'public',
body: 'test'
@@ -38,13 +49,21 @@ describe('Testing Conversation endpoints', () => {
expect(res).to.have.status(200)
expect(res.body).to.haveOwnProperty('UUID')
- conversationUUID = res.body.UUID
+ rootConvoUUID = res.body.UUID
expect(res.body).to.haveOwnProperty('target_uuid')
expect(res.body.target_uuid).to.equal(orgUUID)
+ expect(res.body).to.haveOwnProperty('previous_conversation_uuid')
+ expect(res.body.previous_conversation_uuid).to.be.null
+ expect(res.body).to.haveOwnProperty('next_conversation_uuid')
+ expect(res.body.next_conversation_uuid).to.be.null
+
expect(res.body).to.haveOwnProperty('author_id')
+ expect(res.body.author_id).to.equal(secUserUUID)
+
expect(res.body).to.haveOwnProperty('author_name')
+ expect(res.body.author_name).to.equal('Unknown User')
expect(res.body).to.haveOwnProperty('author_role')
expect(res.body.author_role).to.equal('Secretariat')
@@ -56,6 +75,47 @@ describe('Testing Conversation endpoints', () => {
expect(res.body.body).to.equal('test')
})
})
+ it('Should append a private conversation as Secretariat', async () => {
+ const conversation = {
+ visibility: 'private',
+ body: 'test 2'
+ }
+ const res = await chai
+ .request(app)
+ .post(`/api/conversation/target/${orgUUID}`)
+ .set(constants.headers)
+ .send(conversation)
+
+ expect(res).to.have.status(200)
+
+ expect(res.body).to.haveOwnProperty('UUID')
+ const secondUUID = res.body.UUID
+
+ expect(res.body).to.haveOwnProperty('target_uuid')
+ expect(res.body.target_uuid).to.equal(orgUUID)
+
+ expect(res.body).to.haveOwnProperty('visibility')
+ expect(res.body.visibility).to.equal('private')
+
+ const convoRes = await chai.request(app)
+ .get(`/api/conversation/target/${orgUUID}`)
+ .set(constants.headers)
+
+ expect(convoRes).to.have.status(200)
+
+ expect(convoRes.body).to.be.an('array')
+ expect(convoRes.body).to.have.lengthOf(2)
+
+ const rootMessage = convoRes.body.filter(convo => convo.UUID === rootConvoUUID)[0]
+ expect(rootMessage).to.exist
+ expect(rootMessage.previous_conversation_uuid).to.be.null
+ expect(rootMessage.next_conversation_uuid).to.be.equal(secondUUID)
+
+ expect(res.body).to.haveOwnProperty('previous_conversation_uuid')
+ expect(res.body.previous_conversation_uuid).to.be.equal(rootConvoUUID)
+ expect(res.body).to.haveOwnProperty('next_conversation_uuid')
+ expect(res.body.next_conversation_uuid).to.be.null
+ })
it('Should get all conversations', async () => {
await chai.request(app)
.get('/api/conversation')
@@ -66,10 +126,10 @@ describe('Testing Conversation endpoints', () => {
expect(res.body).to.haveOwnProperty('conversations')
expect(res.body.conversations).to.be.an('array')
- expect(res.body.conversations).to.have.lengthOf(1)
+ expect(res.body.conversations).to.have.lengthOf(2)
})
})
- it('Should get all conversations for target UUID', async () => {
+ it('Should get and see all conversations for target UUID as Secretariat', async () => {
await chai.request(app)
.get(`/api/conversation/target/${orgUUID}`)
.set(constants.headers)
@@ -78,27 +138,11 @@ describe('Testing Conversation endpoints', () => {
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')
+ expect(res.body).to.have.lengthOf(2)
+ res.body.forEach(convo => {
+ expect(convo).to.haveOwnProperty('target_uuid')
+ expect(convo.target_uuid).to.equal(orgUUID)
+ })
})
})
})
@@ -113,19 +157,6 @@ describe('Testing Conversation endpoints', () => {
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/registryOrg.js b/test/integration-tests/org/registryOrg.js
index 7dc509c01..3070970db 100644
--- a/test/integration-tests/org/registryOrg.js
+++ b/test/integration-tests/org/registryOrg.js
@@ -42,7 +42,6 @@ const createNewUserWithNewOrg = async () => {
return { orgShortName, username }
}
-
describe('Testing Secretariat functionality for Orgs', () => {
context('Positive Tests', () => {
it('Secretariat can request a list of all organizations', async () => {
@@ -89,7 +88,7 @@ describe('Testing Secretariat functionality for Orgs', () => {
it('The MITRE CNA has a valid ID quota', async () => {
await chai.request(app)
- .get('/api/registry/org/mitre/id_quota')
+ .get('/api/registry/org/mitre/hard_quota')
.set(secretariatHeaders)
.then((res) => {
expect(res).to.have.status(200)
@@ -114,8 +113,14 @@ describe('Testing Secretariat functionality for Orgs', () => {
expect(res).to.have.status(200)
})
await chai.request(app)
- .put('/api/registry/org/test_registry_org_cna?id_quota=100000')
+ .put('/api/registry/org/test_registry_org_cna')
.set(secretariatHeaders)
+ .send({
+ short_name: 'test_registry_org_cna',
+ long_name: 'Testing Registry Org CNA',
+ hard_quota: 100000,
+ authority: ['CNA']
+ })
.then((res) => {
expect(res).to.have.status(200)
expect(res.body.updated.hard_quota).to.equal(100000)
@@ -166,13 +171,19 @@ describe('Testing Secretariat functionality for Orgs', () => {
})
})
- it('A user\'s username can be updated', async () => {
+ it('A users username can be updated', async function () {
const { orgShortName, username } = await createNewUserWithNewOrg()
const newUsername = uuidv4()
+ let user
+
+ await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body })
await chai.request(app)
- .put(`/api/registry/org/${orgShortName}/user/${username}?new_username=${newUsername}`)
+ .put(`/api/registry/org/${orgShortName}/user/${username}`)
.set(secretariatHeaders)
+ .send(
+ { ...user, username: newUsername }
+ )
.then((res) => {
expect(res).to.have.status(200)
expect(res.body.message).to.equal(`${username} was successfully updated.`)
@@ -181,7 +192,7 @@ describe('Testing Secretariat functionality for Orgs', () => {
// Verify old user does not exist
await chai.request(app)
- .put(`/api/registry/org/${orgShortName}/user/${username}?new_username=${newUsername}`)
+ .get(`/api/registry/org/${orgShortName}/user/${username}`)
.set(secretariatHeaders)
.then((res) => {
expect(res).to.have.status(404)
@@ -189,14 +200,18 @@ describe('Testing Secretariat functionality for Orgs', () => {
})
})
- it('A user\'s organization can be updated', async () => {
+ it('A users organization can be updated', async () => {
const { orgShortName, username } = await createNewUserWithNewOrg()
const newOrgShortName = uuidv4().slice(0, MAX_SHORTNAME_LENGTH)
await postNewOrg(newOrgShortName, newOrgShortName)
+ let user
+ await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body })
+
await chai.request(app)
- .put(`/api/registry/org/${orgShortName}/user/${username}?org_short_name=${newOrgShortName}`)
+ .put(`/api/registry/org/${orgShortName}/user/${username}`)
.set(secretariatHeaders)
+ .send({ ...user, org_short_name: newOrgShortName })
.then((res) => {
expect(res).to.have.status(200)
expect(res.body.message).to.equal(`${username} was successfully updated.`)
@@ -210,15 +225,36 @@ describe('Testing Secretariat functionality for Orgs', () => {
expect(res).to.have.status(200)
expect(res.body.username).to.equal(username)
})
+
+ // Verify user is NOT in the old org
+ await chai.request(app)
+ .get(`/api/registry/org/${orgShortName}/user/${username}`)
+ .set(secretariatHeaders)
+ .then((res) => {
+ expect(res).to.have.status(404)
+ })
})
it('A user\'s personal info can be updated', async () => {
const { orgShortName, username } = await createNewUserWithNewOrg()
const nameUid = uuidv4()
+ let user
+
+ await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body })
+
await chai.request(app)
- .put(`/api/registry/org/${orgShortName}/user/${username}?name.first=${nameUid}&name.last=${nameUid}&name.middle=${nameUid}&name.suffix=${nameUid}`)
+ .put(`/api/registry/org/${orgShortName}/user/${username}`)
.set(secretariatHeaders)
+ .send({
+ ...user,
+ name: {
+ first: nameUid,
+ last: nameUid,
+ middle: nameUid,
+ suffix: nameUid
+ }
+ })
.then((res) => {
expect(res).to.have.status(200)
expect(res.body.updated.name.first).to.equal(nameUid)
@@ -231,11 +267,14 @@ describe('Testing Secretariat functionality for Orgs', () => {
it('A user role can be added', async () => {
const { orgShortName, username } = await createNewUserWithNewOrg()
await chai.request(app)
- .put(`/api/registry/org/${orgShortName}/user/${username}?active_roles.add=ADMIN`)
+ .post(`/api/registry/org/${orgShortName}/user/${username}/grant-role`)
.set(secretariatHeaders)
+ .send({
+ role: 'ADMIN'
+ })
.then((res) => {
expect(res).to.have.status(200)
- expect(res.body.updated.role).to.equal('ADMIN')
+ expect(res.body.message).to.contain('Role ADMIN granted to user')
})
})
@@ -243,19 +282,26 @@ describe('Testing Secretariat functionality for Orgs', () => {
const { orgShortName, username } = await createNewUserWithNewOrg()
// Add role first
await chai.request(app)
- .put(`/api/registry/org/${orgShortName}/user/${username}?active_roles.add=ADMIN`)
+ .post(`/api/registry/org/${orgShortName}/user/${username}/grant-role`)
.set(secretariatHeaders)
+ .send({
+ role: 'ADMIN'
+ })
.then((res) => {
expect(res).to.have.status(200)
+ expect(res.body.message).to.contain('Role ADMIN granted to user')
})
// Then remove it
await chai.request(app)
- .put(`/api/registry/org/${orgShortName}/user/${username}?active_roles.remove=ADMIN`)
+ .post(`/api/registry/org/${orgShortName}/user/${username}/revoke-role`)
.set(secretariatHeaders)
+ .send({
+ role: 'ADMIN'
+ })
.then((res) => {
expect(res).to.have.status(200)
- expect(res.body.updated.role).to.not.equal('ADMIN')
+ expect(res.body.message).to.contain('Role ADMIN revoked from user')
})
})
@@ -424,33 +470,15 @@ describe('Testing Secretariat functionality for Orgs', () => {
it('Fails to update an org that does not exist', async () => {
const nonExistentOrg = 'nonexistent_org'
await chai.request(app)
- .put(`/api/registry/org/${nonExistentOrg}?id_quota=100`)
+ .put(`/api/registry/org/${nonExistentOrg}`)
.set(secretariatHeaders)
+ .send({ hard_quota: 100 })
.then((res) => {
expect(res).to.have.status(404)
expect(res.body.error).to.equal('ORG_DNE_PARAM')
})
})
- const malformedRolesQuery = [
- 'active_roles.add[][a]=CNA',
- 'active_roles.add[][CNA]'
- ]
-
- malformedRolesQuery.forEach((query) => {
- it('Fails to update an org with malformed roles in query', async () => {
- await chai.request(app)
- .put(`/api/registry/org/mitre?${query}`)
- .set(secretariatHeaders)
- .then((res) => {
- expect(res).to.have.status(400)
- expect(res.body.message).to.equal('Parameters were invalid')
- expect(res.body.details[0].param).to.equal('active_roles.add')
- expect(res.body.details[0].msg).to.equal('Parameter must be a one-dimensional array of strings')
- })
- })
- })
-
it('should fail requests from a user that does not exist', async () => {
const fakeIdentifier = uuidv4()
const nonExistentUserHeaders = {
@@ -472,24 +500,30 @@ describe('Testing Secretariat functionality for Orgs', () => {
it('Fails to add a non-existent role to a user', async () => {
const { orgShortName, username } = await createNewUserWithNewOrg()
await chai.request(app)
- .put(`/api/registry/org/${orgShortName}/user/${username}?active_roles.add=MAGNANIMOUS`)
+ .post(`/api/registry/org/${orgShortName}/user/${username}/grant-role`)
.set(secretariatHeaders)
+ .send({
+ role: 'MAGNANIMOUS'
+ })
.then((res) => {
expect(res).to.have.status(400)
expect(res.body.error).to.equal('BAD_INPUT')
- expect(res.body.details[0].msg).to.contain('Invalid role. Valid role')
+ expect(res.body.message).to.contain('Invalid role request')
})
})
it('Fails to remove a non-existent role from a user', async () => {
const { orgShortName, username } = await createNewUserWithNewOrg()
await chai.request(app)
- .put(`/api/registry/org/${orgShortName}/user/${username}?active_roles.remove=FELLOE`)
+ .post(`/api/registry/org/${orgShortName}/user/${username}/revoke-role`)
.set(secretariatHeaders)
+ .send({
+ role: 'FELLOE'
+ })
.then((res) => {
expect(res).to.have.status(400)
expect(res.body.error).to.equal('BAD_INPUT')
- expect(res.body.details[0].msg).to.contain('Invalid role. Valid role')
+ expect(res.body.message).to.contain('Invalid role request')
})
})
})
diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js
index 2b44b831f..d248215dd 100644
--- a/test/integration-tests/org/registryOrgAsOrgAdmin.js
+++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js
@@ -76,19 +76,28 @@ describe('Testing Registry Org as org admin', () => {
adminHeaders['CVE-API-KEY'] = secret
})
- await chai.request(app)
- .get('/api/registry/org/beat_10/user/drocca@test.mitre.org')
- .set(adminHeaders)
- .then((res, err) => {
- expect(err).to.be.undefined
- expect(res).to.have.status(200)
- expect(res.body.role).to.equal('ADMIN')
- })
+ // What do we want to do about this
+ // await chai.request(app)
+ // .get('/api/registry/org/beat_10/user/drocca@test.mitre.org')
+ // .set(adminHeaders)
+ // .then((res, err) => {
+ // expect(err).to.be.undefined
+ // expect(res).to.have.status(200)
+ // expect(res.body.role).to.equal('ADMIN')
+ // })
})
it('Registry: allows admin users to update a user username', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/beat_10/user/second_user@beat_10.mitre.org').set(adminHeaders).then((res) => { user = res.body })
await chai.request(app)
- .put('/api/registry/org/beat_10/user/second_user@beat_10.mitre.org?new_username=second_user_update@beat_10.mitre.org')
+ .put('/api/registry/org/beat_10/user/second_user@beat_10.mitre.org')
.set(adminHeaders)
+ .send(
+ {
+ ...user,
+ username: 'second_user_update@beat_10.mitre.org'
+ }
+ )
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
@@ -96,9 +105,22 @@ describe('Testing Registry Org as org admin', () => {
})
})
it('Registry: allows admin users to update a users name', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/beat_10/user/third_user@beat_10.mitre.org').set(adminHeaders).then((res) => { user = res.body })
await chai.request(app)
- .put('/api/registry/org/beat_10/user/third_user@beat_10.mitre.org?name.first=t&name.last=e&name.middle=s&name.suffix=t')
+ .put('/api/registry/org/beat_10/user/third_user@beat_10.mitre.org')
.set(adminHeaders)
+ .send(
+ {
+ ...user,
+ name: {
+ first: 't',
+ last: 'e',
+ middle: 's',
+ suffix: 't'
+ }
+ }
+ )
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
@@ -109,9 +131,20 @@ describe('Testing Registry Org as org admin', () => {
})
})
it('Registry: allows admin users to update their own name', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/beat_10/user/drocca@test.mitre.org').set(adminHeaders).then((res) => { user = res.body })
await chai.request(app)
.put('/api/registry/org/beat_10/user/drocca@test.mitre.org?name.first=t&name.last=e&name.middle=s&name.suffix=t')
.set(adminHeaders)
+ .send({
+ ...user,
+ name: {
+ first: 't',
+ last: 'e',
+ middle: 's',
+ suffix: 't'
+ }
+ })
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
@@ -123,22 +156,32 @@ describe('Testing Registry Org as org admin', () => {
})
it('Registry: allows admin users to add a users role', async () => {
await chai.request(app)
- .put('/api/registry/org/beat_10/user/third_user@beat_10.mitre.org?active_roles.add=admin')
+ .post('/api/registry/org/beat_10/user/third_user@beat_10.mitre.org/grant-role')
.set(adminHeaders)
+ .send(
+ {
+ role: 'ADMIN'
+ }
+ )
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
- expect(res.body.updated.role).to.be.equal('ADMIN')
+ expect(res.body.message).to.contain('Role ADMIN granted to user')
})
})
it('Registry: allows admin users to remove a users role', async () => {
await chai.request(app)
- .put('/api/registry/org/beat_10/user/third_user@beat_10.mitre.org?active_roles.remove=admin')
+ .post('/api/registry/org/beat_10/user/third_user@beat_10.mitre.org/revoke-role')
.set(adminHeaders)
+ .send(
+ {
+ role: 'ADMIN'
+ }
+ )
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
- expect(res.body.updated.role).to.not.be.equal('ADMIN')
+ expect(res.body.message).to.contain('Role ADMIN revoked from user')
})
})
it('Registry: page must be a positive int', async () => {
@@ -182,7 +225,7 @@ describe('Testing Registry Org as org admin', () => {
})
it('Registry: services api allows org admins to get their own org quota', async () => {
await chai.request(app)
- .get(`/api/registry/org/${shortName}/id_quota`)
+ .get(`/api/registry/org/${shortName}/hard_quota`)
.set(adminHeaders)
.then((res, err) => {
expect(err).to.be.undefined
@@ -225,8 +268,11 @@ describe('Testing Registry Org as org admin', () => {
})
it('Registry: does not allow an admin to self demote', async () => {
await chai.request(app)
- .put('/api/registry/org/beat_10/user/drocca@test.mitre.org?active_roles.remove=admin')
+ .post('/api/registry/org/beat_10/user/drocca@test.mitre.org/revoke-role')
.set(adminHeaders)
+ .send({
+ role: 'ADMIN'
+ })
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(403)
@@ -234,10 +280,13 @@ describe('Testing Registry Org as org admin', () => {
})
})
it('Registry: Services api prevents org admins from updating a users username if that user already exists', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com').set(adminHeaders).then((res) => { user = res.body })
await chai.request(app)
- .put('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com?new_username=drocca@test.mitre.org')
+ .put('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com')
.set(adminHeaders)
.send({
+ ...user,
username: userId
})
.then((res, err) => {
@@ -390,7 +439,7 @@ describe('Testing Registry Org as org admin', () => {
})
it('Registry: services api rejects requests for org quota by admin of another org', async () => {
await chai.request(app)
- .get('/api/registry/org/range_4/id_quota')
+ .get('/api/registry/org/range_4/hard_quota')
.set(adminHeaders)
.then((res, err) => {
expect(err).to.be.undefined
@@ -400,7 +449,7 @@ describe('Testing Registry Org as org admin', () => {
})
it('Registry: services api rejects requests for secretariat quota by non-secretariat users', async () => {
await chai.request(app)
- .get('/api/registry/org/mitre/id_quota')
+ .get('/api/registry/org/mitre/hard_quota')
.set(adminHeaders)
.then((res, err) => {
expect(err).to.be.undefined
diff --git a/test/integration-tests/org/regularUsersTestRegistry.js b/test/integration-tests/org/regularUsersTestRegistry.js
index 6ffe1986d..348df2336 100644
--- a/test/integration-tests/org/regularUsersTestRegistry.js
+++ b/test/integration-tests/org/regularUsersTestRegistry.js
@@ -18,11 +18,26 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with
it('regular user can update their name', async () => {
const org = constants.nonSecretariatUserHeaders['CVE-API-ORG']
const user = constants.nonSecretariatUserHeaders['CVE-API-USER']
+
+ let previousBody
+ await chai.request(app).get(`/api/registry/org/${org}/user/${user}`)
+ .set(constants.nonSecretariatUserHeaders)
+ .then((res) => { previousBody = res.body })
+
await chai.request(app)
- .put(`/api/registry/org/${org}/user/${user}?name.first=aaa&name.last=bbb&name.middle=ccc&name.suffix=ddd`)
+ .put(`/api/registry/org/${org}/user/${user}`)
.set(constants.nonSecretariatUserHeaders)
- .send({
- })
+ .send(
+ {
+ ...previousBody,
+ name: {
+ first: 'aaa',
+ last: 'bbb',
+ middle: 'ccc',
+ suffix: 'ddd'
+ }
+ }
+ )
.then((res) => {
expect(res).to.have.status(200)
expect(res.body.updated.name.first).contain('aaa')
@@ -51,52 +66,84 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with
const newUsername = faker.datatype.uuid()
const org = constants.nonSecretariatUserHeaders['CVE-API-ORG']
const user = constants.nonSecretariatUserHeaders['CVE-API-USER']
+
+ let previousBody
+ await chai.request(app).get(`/api/registry/org/${org}/user/${user}`)
+ .set(constants.nonSecretariatUserHeaders)
+ .then((res) => { previousBody = res.body })
+
await chai.request(app)
- .put(`/api/registry/org/${org}/user/${user}?new_username=${newUsername}`)
+ .put(`/api/registry/org/${org}/user/${user}`)
.set(constants.nonSecretariatUserHeaders)
.send({
+ ...previousBody,
+ username: newUsername
})
.then((res) => {
- expect(res).to.have.status(403)
- expect(res.body.error).to.contain('NOT_ORG_ADMIN_OR_SECRETARIAT_UPDATE')
+ // NOTE: We are changing this error message to be more succinct
+ expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD')
+ expect(res).to.have.status(400)
})
})
it('regular user cannot update information of another user of the same organization', async () => {
const newUsername = faker.datatype.uuid()
const org = constants.nonSecretariatUserHeaders['CVE-API-ORG']
const user2 = constants.nonSecretariatUserHeaders2['CVE-API-USER']
+
+ let previousBody
+ await chai.request(app).get(`/api/registry/org/${org}/user/${user2}`)
+ .set(constants.nonSecretariatUserHeaders)
+ .then((res) => { previousBody = res.body })
+
await chai.request(app)
- .put(`/api/registry/org/${org}/user/${user2}?new_username=${newUsername}`)
+ .put(`/api/registry/org/${org}/user/${user2}`)
.set(constants.nonSecretariatUserHeaders)
.send({
+ ...previousBody,
+ username: newUsername
})
.then((res) => {
expect(res).to.have.status(403)
expect(res.body.error).to.contain('NOT_SAME_USER_OR_SECRETARIAT')
})
})
- it("regular users cannot update a user's username if that user already exist", async () => {
+ it("regular users cannot update a user's username if that user already exists", async () => {
const org = constants.nonSecretariatUserHeaders['CVE-API-ORG']
const user1 = constants.nonSecretariatUserHeaders['CVE-API-USER']
const user2 = constants.nonSecretariatUserHeaders2['CVE-API-USER']
+ let previousBody
+ await chai.request(app).get(`/api/registry/org/${org}/user/${user1}`)
+ .set(constants.nonSecretariatUserHeaders)
+ .then((res) => { previousBody = res.body })
+
await chai.request(app)
- .put(`/api/registry/org/${org}/user/${user1}?new_username=${user2}`)
+ .put(`/api/registry/org/${org}/user/${user1}`)
.set(constants.nonSecretariatUserHeaders)
.send({
+ ...previousBody,
+ username: user2
})
.then((res) => {
- expect(res).to.have.status(403)
- expect(res.body.error).to.contain('NOT_ORG_ADMIN_OR_SECRETARIAT_UPDATE')
+ expect(res).to.have.status(400)
+ expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD')
})
})
it('regular users cannot update organization', async () => {
const org1 = constants.nonSecretariatUserHeaders['CVE-API-ORG']
const user = constants.nonSecretariatUserHeaders['CVE-API-USER']
const org2 = faker.datatype.uuid().slice(0, MAX_SHORTNAME_LENGTH)
+
+ let previousBody
+ await chai.request(app).get(`/api/registry/org/${org1}/user/${user}`)
+ .set(constants.nonSecretariatUserHeaders)
+ .then((res) => { previousBody = res.body })
+
await chai.request(app)
- .put(`/api/registry/org/${org1}/user/${user}?org_short_name=${org2}`)
+ .put(`/api/registry/org/${org1}/user/${user}`)
.set(constants.nonSecretariatUserHeaders)
.send({
+ ...previousBody,
+ org_short_name: org2
})
.then((res) => {
expect(res).to.have.status(403)
@@ -106,23 +153,32 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with
it('regular user cannot change its own active state', async () => {
const org = constants.nonSecretariatUserHeaders['CVE-API-ORG']
const user = constants.nonSecretariatUserHeaders['CVE-API-USER']
+
+ let previousBody
+ await chai.request(app).get(`/api/registry/org/${org}/user/${user}`)
+ .set(constants.nonSecretariatUserHeaders)
+ .then((res) => { previousBody = res.body })
+
await chai.request(app)
- .put(`/api/registry/org/${org}/user/${user}?active=false`)
+ .put(`/api/registry/org/${org}/user/${user}`)
.set(constants.nonSecretariatUserHeaders)
.send({
+ ...previousBody,
+ status: 'inactive'
})
.then((res) => {
- expect(res).to.have.status(403)
- expect(res.body.error).to.contain('NOT_ORG_ADMIN_OR_SECRETARIAT_UPDATE')
+ expect(res).to.have.status(400)
+ expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD')
})
})
it('regular users cannot add role', async () => {
const org = constants.nonSecretariatUserHeaders['CVE-API-ORG']
const user = constants.nonSecretariatUserHeaders['CVE-API-USER']
await chai.request(app)
- .put(`/api/registry/org/${org}/user/${user}?active_roles.add=admin`)
+ .post(`/api/registry/org/${org}/user/${user}/grant-role`)
.set(constants.nonSecretariatUserHeaders)
.send({
+ role: 'ADMIN'
})
.then((res) => {
expect(res).to.have.status(403)
@@ -133,9 +189,10 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with
const org = constants.nonSecretariatUserHeaders['CVE-API-ORG']
const user = constants.nonSecretariatUserHeaders['CVE-API-USER']
await chai.request(app)
- .put(`/api/registry/org/${org}/user/${user}?active_roles.remove=admin`)
+ .post(`/api/registry/org/${org}/user/${user}/revoke-role`)
.set(constants.nonSecretariatUserHeaders)
.send({
+ role: 'ADMIN'
})
.then((res) => {
expect(res).to.have.status(403)
@@ -349,7 +406,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with
})
.then((res) => {
expect(res).to.have.status(403)
- expect(res.body.error).to.contain('SECRETARIAT_ONLY')
+ expect(res.body.error).to.contain('NOT_SAME_ORG_OR_SECRETARIAT')
})
})
})
@@ -389,7 +446,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with
it("regular users can see their organization's cve id quota", async () => {
const org = constants.nonSecretariatUserHeaders['CVE-API-ORG']
await chai.request(app)
- .get(`/api/registry/org/${org}/id_quota`)
+ .get(`/api/registry/org/${org}/hard_quota`)
.set(constants.nonSecretariatUserHeaders)
.send({
})
@@ -429,7 +486,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with
it("regular users cannot see an organization's cve id quota they don't belong to", async () => {
const org = constants.nonSecretariatUserHeaders3['CVE-API-ORG']
await chai.request(app)
- .get(`/api/registry/org/${org}/id_quota`)
+ .get(`/api/registry/org/${org}/hard_quota`)
.set(constants.nonSecretariatUserHeaders)
.send({
})
diff --git a/test/integration-tests/registry-org/createUserByOrgTest.js b/test/integration-tests/registry-org/createUserByOrgTest.js
index d6265509d..9397eb8db 100644
--- a/test/integration-tests/registry-org/createUserByOrgTest.js
+++ b/test/integration-tests/registry-org/createUserByOrgTest.js
@@ -82,30 +82,6 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => {
})
})
- // Negative test: Requester not admin or secretariat
- // Right now we are locking down to just secretariat
- it.skip('Should not create a user if requester is not an admin or secretariat', (done) => {
- const orgShortName = 'mitre'
- const newUser = {
- username: 'testuser3@example.com',
- name: {
- first: 'Test',
- last: 'User'
- }
- }
-
- chai.request(app)
- .post(`/api/registryOrg/${orgShortName}/user`)
- .set(constants.nonSecretariatUserHeaders)
- .send(newUser)
- .end((err, res) => {
- expect(err).to.be.null
- expect(res).to.have.status(403)
- expect(res.body).to.have.property('message').equal('Users can only be created by the Secretariat or Org Admin.')
- done()
- })
- })
-
// Negative test: Validation error (missing username)
it('Should not create a user with a missing username', (done) => {
const orgShortName = 'mitre'
diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js
index 0145a125b..e3eabff16 100644
--- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js
+++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js
@@ -71,7 +71,7 @@ describe('Testing Joint approval', () => {
let reviewUUID
it('Create an org to use for testing', async () => {
await chai.request(app)
- .post('/api/registryOrg')
+ .post('/api/registry/org')
.set(secretariatHeaders)
.send(testRegistryOrgForReview)
.then((res, err) => {
@@ -114,7 +114,7 @@ describe('Testing Joint approval', () => {
})
it('Attempt to change the short name of the org', async () => {
await chai.request(app)
- .put('/api/registryOrg/non_secretariat_org')
+ .put('/api/registry/org/non_secretariat_org')
.set(nonAdminHeaders)
.send({ ...testRegistryOrgForReview, short_name: 'new_non_secretariat_org', hard_quota: 10000 })
.then((res) => {
@@ -217,7 +217,7 @@ describe('Testing Joint approval', () => {
})
it('Attempt to change the short name of the org', async () => {
await chai.request(app)
- .put('/api/registryOrg/non_with_comments')
+ .put('/api/registry/org/non_with_comments')
.set(nonAdminHeaders2)
.send({ ...testRegistryOrgForReviewWithComments, short_name: 'new_non_with_comments', hard_quota: 10000 })
.then((res) => {
@@ -252,7 +252,7 @@ describe('Testing Joint approval', () => {
})
it('Secretariat leaves a public comment on the org review', async () => {
await chai.request(app)
- .post(`/api/conversation/target/${reviewUUID}`)
+ .post(`/api/conversation/target/${orgUUID}`)
.set(secretariatHeaders)
.send({
visibility: 'public',
@@ -266,9 +266,9 @@ describe('Testing Joint approval', () => {
expect(res.body.body).to.equal('This is a comment left by the secretariat.')
})
})
- it('Secretariat leaves a private on the org review', async () => {
+ it('Secretariat leaves a private comment on the org review', async () => {
await chai.request(app)
- .post(`/api/conversation/target/${reviewUUID}`)
+ .post(`/api/conversation/target/${orgUUID}`)
.set(secretariatHeaders)
.send({
visibility: 'private',
diff --git a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js
new file mode 100644
index 000000000..d2c056768
--- /dev/null
+++ b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js
@@ -0,0 +1,65 @@
+/* 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 testNullRemovalOrg = {
+ short_name: 'test_null_removal',
+ long_name: 'Test Null Removal Org',
+ authority: ['CNA'],
+ hard_quota: 1000,
+ contact_info: {
+ website: null, // Should be removed
+ org_email: undefined // Should be removed (or not present)
+ }
+}
+
+describe('Testing Deep Remove Empty in Create Org', () => {
+ context('Positive Tests', () => {
+ it('Creates a registry org and verifies null values are removed', async () => {
+ await chai.request(app)
+ .post('/api/registryOrg')
+ .set(secretariatHeaders)
+ .send(testNullRemovalOrg)
+ .then((res, err) => {
+ expect(err).to.be.undefined
+ if (res.status !== 200) {
+ console.log('Test failed with status:', res.status)
+ console.log('Response body:', JSON.stringify(res.body, null, 2))
+ }
+ expect(res).to.have.status(200)
+
+ expect(res.body).to.haveOwnProperty('created')
+ const createdOrg = res.body.created
+
+ expect(createdOrg).to.haveOwnProperty('short_name')
+ expect(createdOrg.short_name).to.equal(testNullRemovalOrg.short_name)
+
+ // Verify contact_info exists but does NOT contain website or org_email
+ // Ideally if contact_info becomes empty, deepRemoveEmpty might remove the whole object if it recurses well.
+ // Let's check what happened.
+ if (createdOrg.contact_info) {
+ expect(createdOrg.contact_info).to.not.have.property('website')
+ expect(createdOrg.contact_info).to.not.have.property('org_email')
+ // If deepRemoveEmpty works on nested empty objects, contact_info might be gone or empty.
+ expect(Object.keys(createdOrg.contact_info)).to.be.empty
+ } else {
+ // This is also acceptable if deepRemoveEmpty removes empty objects
+ expect(createdOrg).to.not.have.property('contact_info')
+ }
+ })
+ })
+
+ after(async () => {
+ // Cleanup: Delete the created org
+ await chai.request(app)
+ .delete('/api/registryOrg/test_null_removal')
+ .set(secretariatHeaders)
+ })
+ })
+})
diff --git a/test/integration-tests/review-object/reviewObjectTest.js b/test/integration-tests/review-object/reviewObjectTest.js
index e2fd6c447..76d3457b5 100644
--- a/test/integration-tests/review-object/reviewObjectTest.js
+++ b/test/integration-tests/review-object/reviewObjectTest.js
@@ -9,6 +9,10 @@ const app = require('../../../src/index.js')
describe('Review Object Controller Integration Tests', () => {
let orgUUID
let reviewUUID
+ let approveTestReviewUUID
+ let rejectTestReviewUUID
+ let autoApproveReviewUUID
+ let autoRejectReviewUUID
context('Positive Tests', () => {
it('Creates an organization to use for review object tests', async () => {
@@ -24,13 +28,13 @@ describe('Review Object Controller Integration Tests', () => {
})
it('Creates a review object for the organization', async () => {
- const reviewObject = constants.testRegistryOrg2
+ const reviewObject = { ...constants.testRegistryOrg2 }
reviewObject.UUID = orgUUID
const res = await chai
.request(app)
.post('/api/review/org/')
.set({ ...constants.headers })
- .send(constants.testRegistryOrg2)
+ .send(reviewObject)
expect(res).to.have.status(200)
expect(res.body).to.have.property('uuid')
expect(res.body).to.have.property('target_object_uuid', orgUUID)
@@ -56,19 +60,30 @@ describe('Review Object Controller Integration Tests', () => {
expect(res.body).to.have.property('uuid', reviewUUID)
})
+ it('Retrieves the review object by review UUID', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/byUUID/${reviewUUID}`)
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('uuid', reviewUUID)
+ expect(res.body).to.have.property('target_object_uuid', orgUUID)
+ })
+
it('Retrieves all review objects', async () => {
const res = await chai
.request(app)
.get('/api/review/orgs')
.set({ ...constants.headers })
expect(res).to.have.status(200)
- expect(res.body).to.be.an('array')
- const found = res.body.find(obj => obj.uuid === reviewUUID)
+ expect(res.body).to.haveOwnProperty('reviewObjects')
+ expect(res.body.reviewObjects).to.be.an('array')
+ const found = res.body.reviewObjects.find(obj => obj.uuid === reviewUUID)
expect(found).to.exist
})
it('Updates the review object with new short_name', async () => {
- const reviewObject = constants.testRegistryOrg2
+ const reviewObject = { ...constants.testRegistryOrg2 }
reviewObject.UUID = orgUUID
reviewObject.short_name = 'updated_org'
const res = await chai
@@ -80,6 +95,288 @@ describe('Review Object Controller Integration Tests', () => {
expect(res.body).to.have.property('uuid', reviewUUID)
expect(res.body.new_review_data).to.have.property('short_name', 'updated_org')
})
+
+ it('Retrieves review objects with page parameter', async () => {
+ const res = await chai
+ .request(app)
+ .get('/api/review/orgs?page=1')
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('reviewObjects')
+ expect(res.body.reviewObjects).to.be.an('array')
+ })
+
+ it('Retrieves review objects filtered by pending status', async () => {
+ const res = await chai
+ .request(app)
+ .get('/api/review/orgs?status=pending')
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('reviewObjects')
+ res.body.reviewObjects.forEach(obj => {
+ expect(obj.status).to.equal('pending')
+ })
+ })
+
+ it('Retrieves review objects filtered by approved status', async () => {
+ const res = await chai
+ .request(app)
+ .get('/api/review/orgs?status=approved')
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('reviewObjects')
+ res.body.reviewObjects.forEach(obj => {
+ expect(obj.status).to.equal('approved')
+ })
+ })
+
+ it('Retrieves review objects filtered by rejected status', async () => {
+ const res = await chai
+ .request(app)
+ .get('/api/review/orgs?status=rejected')
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('reviewObjects')
+ res.body.reviewObjects.forEach(obj => {
+ expect(obj.status).to.equal('rejected')
+ })
+ })
+
+ it('Retrieves review objects with both page and status parameters', async () => {
+ const res = await chai
+ .request(app)
+ .get('/api/review/orgs?page=1&status=pending')
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('reviewObjects')
+ })
+
+ it('Retrieves review history for an organization', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.testRegistryOrg2.short_name}/reviews`)
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('reviewObjects')
+ expect(res.body.reviewObjects).to.be.an('array')
+ })
+
+ it('Retrieves review history with pagination', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.testRegistryOrg2.short_name}/reviews?page=1`)
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('reviewObjects')
+ })
+
+ it('Retrieves review history with conversations included', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.testRegistryOrg2.short_name}/reviews?include_conversations=true`)
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('reviewObjects')
+ if (res.body.reviewObjects.length > 0) {
+ expect(res.body.reviewObjects[0]).to.have.property('conversation')
+ }
+ })
+
+ it('Nonsecretariat user can update an organization, review object gets created', async () => {
+ const updateData = {}
+ updateData.short_name = constants.existingOrg.short_name
+ updateData.long_name = 'Approve Test Organization'
+ updateData.hard_quota = 123
+ const res = await chai
+ .request(app)
+ .put(`/api/registry/org/${constants.existingOrg.short_name}`)
+ .set({ ...constants.nonSecretariatUserHeaders2 })
+ .send(updateData)
+ expect(res).to.have.status(200)
+ expect(res.body.updated.hard_quota).to.equal(123)
+
+ const reviewRes = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.existingOrg.short_name}`)
+ .set({ ...constants.headers })
+ expect(reviewRes).to.have.status(200)
+ expect(reviewRes.body).to.have.property('uuid')
+ expect(reviewRes.body.status).to.equal('pending')
+ expect(reviewRes.body).to.have.nested.property('new_review_data.long_name', 'Approve Test Organization')
+ expect(reviewRes.body).to.have.nested.property('new_review_data.hard_quota', 123)
+ approveTestReviewUUID = reviewRes.body.uuid
+ })
+
+ it('Approves a review object and updates the organization', async () => {
+ const res = await chai
+ .request(app)
+ .put(`/api/review/org/${approveTestReviewUUID}/approve`)
+ .set({ ...constants.headers })
+ .send({})
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('long_name', 'Approve Test Organization')
+ })
+
+ it('Verifies the review object status is now approved', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/byUUID/${approveTestReviewUUID}`)
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('status', 'approved')
+ })
+
+ it('Create new review object for rejection testing', async () => {
+ const updateData = {}
+ updateData.short_name = constants.existingOrg.short_name
+ updateData.long_name = 'Reject Test Organization'
+ updateData.hard_quota = 456
+ const res = await chai
+ .request(app)
+ .put(`/api/registry/org/${constants.existingOrg.short_name}`)
+ .set({ ...constants.nonSecretariatUserHeaders2 })
+ .send(updateData)
+ expect(res).to.have.status(200)
+ expect(res.body.updated.long_name).to.not.equal('Reject Test Organization')
+
+ const reviewRes = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.existingOrg.short_name}`)
+ .set({ ...constants.headers })
+ expect(reviewRes).to.have.status(200)
+ expect(reviewRes.body).to.have.property('uuid')
+ expect(reviewRes.body.status).to.equal('pending')
+ expect(reviewRes.body).to.have.nested.property('new_review_data.long_name', 'Reject Test Organization')
+ rejectTestReviewUUID = reviewRes.body.uuid
+ })
+
+ it('Rejects a review object', async () => {
+ const res = await chai
+ .request(app)
+ .put(`/api/review/org/${rejectTestReviewUUID}/reject`)
+ .set({ ...constants.headers })
+ .send({})
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('status', 'rejected')
+ })
+
+ it('Verifies the rejected review object status', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/byUUID/${rejectTestReviewUUID}`)
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('status', 'rejected')
+ })
+
+ it('Admin can review history for its own organization', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.existingOrg.short_name}/reviews`)
+ .set({ ...constants.nonSecretariatUserHeaders2 })
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('reviewObjects')
+ expect(res.body.reviewObjects).to.be.an('array')
+ })
+
+ // ------------------------------------------------------------------------------------------------
+ it('Non-secretariat updates org, creates review object for auto-approve test', async () => {
+ const updateData = {
+ short_name: constants.existingOrg.short_name,
+ long_name: 'Auto Approve Test Org',
+ hard_quota: 789
+ }
+ const res = await chai
+ .request(app)
+ .put(`/api/registry/org/${constants.existingOrg.short_name}`)
+ .set({ ...constants.nonSecretariatUserHeaders2 })
+ .send(updateData)
+ expect(res).to.have.status(200)
+
+ const reviewRes = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.existingOrg.short_name}`)
+ .set({ ...constants.headers })
+ expect(reviewRes).to.have.status(200)
+ expect(reviewRes.body).to.have.property('uuid')
+ expect(reviewRes.body.status).to.equal('pending')
+ expect(reviewRes.body).to.have.nested.property('new_review_data.long_name', 'Auto Approve Test Org')
+ expect(reviewRes.body).to.have.nested.property('new_review_data.hard_quota', 789)
+ autoApproveReviewUUID = reviewRes.body.uuid
+ })
+
+ it('Secretariat updates org with matching values, review object gets auto-approved', async () => {
+ const updateData = {
+ short_name: constants.existingOrg.short_name,
+ long_name: 'Auto Approve Test Org',
+ hard_quota: 789
+ }
+ const res = await chai
+ .request(app)
+ .put(`/api/registryOrg/${constants.existingOrg.short_name}`)
+ .set({ ...constants.headers })
+ .send(updateData)
+ expect(res).to.have.status(200)
+ expect(res.body.updated.long_name).to.equal('Auto Approve Test Org')
+ expect(res.body.updated.hard_quota).to.equal(789)
+
+ const reviewRes = await chai
+ .request(app)
+ .get(`/api/review/byUUID/${autoApproveReviewUUID}`)
+ .set({ ...constants.headers })
+ expect(reviewRes).to.have.status(200)
+ expect(reviewRes.body).to.have.property('status', 'approved')
+ })
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ it('Non-secretariat updates org, creates review object for auto-reject test', async () => {
+ const updateData = {
+ short_name: constants.existingOrg.short_name,
+ long_name: 'Auto Reject Pending Org',
+ hard_quota: 999
+ }
+ const res = await chai
+ .request(app)
+ .put(`/api/registry/org/${constants.existingOrg.short_name}`)
+ .set({ ...constants.nonSecretariatUserHeaders2 })
+ .send(updateData)
+ expect(res).to.have.status(200)
+
+ const reviewRes = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.existingOrg.short_name}`)
+ .set({ ...constants.headers })
+ expect(reviewRes).to.have.status(200)
+ expect(reviewRes.body).to.have.property('uuid')
+ expect(reviewRes.body.status).to.equal('pending')
+ expect(reviewRes.body).to.have.nested.property('new_review_data.long_name', 'Auto Reject Pending Org')
+ expect(reviewRes.body).to.have.nested.property('new_review_data.hard_quota', 999)
+ autoRejectReviewUUID = reviewRes.body.uuid
+ })
+
+ it('Secretariat updates org with different values, review object gets auto-rejected', async () => {
+ const updateData = {
+ short_name: constants.existingOrg.short_name,
+ long_name: 'Test Organization',
+ hard_quota: 111
+ }
+ const res = await chai
+ .request(app)
+ .put(`/api/registry/org/${constants.existingOrg.short_name}`)
+ .set({ ...constants.headers })
+ .send(updateData)
+ expect(res).to.have.status(200)
+ expect(res.body.updated.long_name).to.equal('Test Organization')
+ expect(res.body.updated.hard_quota).to.equal(111)
+
+ const reviewRes = await chai
+ .request(app)
+ .get(`/api/review/byUUID/${autoRejectReviewUUID}`)
+ .set({ ...constants.headers })
+ expect(reviewRes).to.have.status(200)
+ expect(reviewRes.body).to.have.property('status', 'rejected')
+ })
+ // ------------------------------------------------------------------------------------------------
})
context('Negative Tests', () => {
@@ -89,6 +386,109 @@ describe('Review Object Controller Integration Tests', () => {
.get('/api/review/org/nonexistent-org')
.set({ ...constants.headers })
expect(res).to.have.status(404)
+ expect(res.body.message).to.contain('No pending review object exists for this organization')
+ })
+
+ it('Returns 404 when approving non-existent review object', async () => {
+ const fakeUUID = '00000000-0000-0000-0000-000000000000'
+ const res = await chai
+ .request(app)
+ .put(`/api/review/org/${fakeUUID}/approve`)
+ .set({ ...constants.headers })
+ .send({})
+ expect(res).to.have.status(404)
+ expect(res.body.message).to.equal(`No review object found with UUID ${fakeUUID}`)
+ })
+
+ it('Returns 404 when rejecting non-existent review object', async () => {
+ const fakeUUID = '00000000-0000-0000-0000-000000000000'
+ const res = await chai
+ .request(app)
+ .put(`/api/review/org/${fakeUUID}/reject`)
+ .set({ ...constants.headers })
+ .send({})
+ expect(res).to.have.status(404)
+ expect(res.body.message).to.equal(`No review object found with UUID ${fakeUUID}`)
+ })
+
+ it('Returns 404 when updating non-existent review object', async () => {
+ const fakeUUID = '00000000-0000-0000-0000-000000000000'
+ const res = await chai
+ .request(app)
+ .put(`/api/review/org/${fakeUUID}`)
+ .set({ ...constants.headers })
+ .send({ short_name: 'test', long_name: 'Test Org', hard_quota: 100 })
+ expect(res).to.have.status(404)
+ })
+
+ it('Returns 404 when getting review object by non-existent UUID', async () => {
+ const fakeUUID = '00000000-0000-0000-0000-000000000000'
+ const res = await chai
+ .request(app)
+ .get(`/api/review/byUUID/${fakeUUID}`)
+ .set({ ...constants.headers })
+ expect(res).to.have.status(200)
+ expect(res.body).to.be.null
+ })
+
+ it('Returns 404 for review history of non-existent organization', async () => {
+ const res = await chai
+ .request(app)
+ .get('/api/review/org/nonexistent_org_12345/reviews')
+ .set({ ...constants.headers })
+ expect(res).to.have.status(404)
+ })
+
+ it('Non-secretariat user cannot access review objects list', async () => {
+ const res = await chai
+ .request(app)
+ .get('/api/review/orgs')
+ .set({ ...constants.nonSecretariatUserHeaders })
+ expect(res).to.have.status(403)
+ })
+
+ it('Non-secretariat user cannot create review object', async () => {
+ const res = await chai
+ .request(app)
+ .post('/api/review/org/')
+ .set({ ...constants.nonSecretariatUserHeaders })
+ .send({ short_name: 'test', UUID: orgUUID })
+ expect(res).to.have.status(403)
+ })
+
+ it('Non-secretariat user cannot update review object', async () => {
+ const res = await chai
+ .request(app)
+ .put(`/api/review/org/${reviewUUID}`)
+ .set({ ...constants.nonSecretariatUserHeaders })
+ .send({ short_name: 'test' })
+ expect(res).to.have.status(403)
+ })
+
+ it('Non-secretariat user cannot approve review object', async () => {
+ const res = await chai
+ .request(app)
+ .put(`/api/review/org/${reviewUUID}/approve`)
+ .set({ ...constants.nonSecretariatUserHeaders })
+ .send({})
+ expect(res).to.have.status(403)
+ })
+
+ it('Non-secretariat user cannot reject review object', async () => {
+ const res = await chai
+ .request(app)
+ .put(`/api/review/org/${reviewUUID}/reject`)
+ .set({ ...constants.nonSecretariatUserHeaders })
+ .send({})
+ expect(res).to.have.status(403)
+ })
+
+ it('Non-secretariat user cannot access pending review object by org identifier', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.testRegistryOrg2.short_name}`)
+ .set({ ...constants.nonSecretariatUserHeaders })
+ expect(res).to.have.status(403)
})
})
})
diff --git a/test/integration-tests/user/regularUserUpdateTest.js b/test/integration-tests/user/regularUserUpdateTest.js
new file mode 100644
index 000000000..7be104954
--- /dev/null
+++ b/test/integration-tests/user/regularUserUpdateTest.js
@@ -0,0 +1,109 @@
+const chai = require('chai')
+const chaiHttp = require('chai-http')
+const expect = chai.expect
+const app = require('../../../src/index.js')
+
+chai.use(chaiHttp)
+
+const regularUserHeaders = {
+ 'CVE-API-ORG': 'win_5',
+ 'CVE-API-Key': 'TCF25YM-39C4H6D-KA32EGF-V5XSHN3',
+ 'CVE-API-USER': 'jasminesmith@win_5.com'
+}
+
+describe('Regular User Self-Update Tests', () => {
+ let user
+ beforeEach(async () => {
+ // get the jasmines user
+ await chai.request(app)
+ .get('/api/registry/org/win_5/user/jasminesmith@win_5.com')
+ .set(regularUserHeaders)
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.username).to.equal('jasminesmith@win_5.com')
+ user = res.body
+ })
+ })
+ it('Should allow regular user to update their own contact info (name)', async () => {
+ await chai.request(app)
+ .put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
+ .set(regularUserHeaders)
+ .send({
+ ...user,
+ name: {
+ first: 'JasmineUpdated',
+ last: 'Smith'
+ }
+ })
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.updated.name.first).to.equal('JasmineUpdated')
+ })
+ })
+
+ it('Should return 400 when regular user tries to update restricted field (status)', async () => {
+ await chai.request(app)
+ .put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
+ .set(regularUserHeaders)
+ .send({
+ ...user,
+ name: {
+ first: 'Jasmine',
+ last: 'Smith'
+ },
+ status: 'inactive' // Trying to deactivate self should be restricted
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD')
+ expect(res.body.message).to.contain('Regular users can only update their contact info')
+ })
+ })
+
+ it('Should return 400 when regular user tries to update restricted field (roles)', async () => {
+ await chai.request(app)
+ .put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
+ .set(regularUserHeaders)
+ .send({
+ ...user,
+ name: {
+ first: 'Jasmine',
+ last: 'Smith'
+ },
+ authority: ['ADMIN']
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD')
+ expect(res.body.message).to.contain('Regular users can only update their contact info')
+ })
+ })
+
+ it('Should allow update if restricted field is sent but unchanged', async () => {
+ // First get the user to know current state
+ let currentUser
+ await chai.request(app)
+ .get('/api/registry/org/win_5/user/jasminesmith@win_5.com')
+ .set(regularUserHeaders)
+ .then((res) => {
+ currentUser = res.body
+ })
+
+ await chai.request(app)
+ .put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
+ .set(regularUserHeaders)
+ .send({
+ ...user,
+ name: {
+ first: 'JasmineUnchangedTest',
+ last: 'Smith'
+ },
+ username: currentUser.username, // Sending same username
+ status: currentUser.status // Sending same status
+ })
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.updated.name.first).to.equal('JasmineUnchangedTest')
+ })
+ })
+})
diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js
index 85e23b22b..d885536af 100644
--- a/test/integration-tests/user/updateUserTest.js
+++ b/test/integration-tests/user/updateUserTest.js
@@ -20,9 +20,18 @@ describe('Testing Edit user endpoint', () => {
})
})
it('Should return 200 when only name changes are done with registry enabled', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body })
await chai.request(app)
- .put('/api/registry/org/win_5/user/jasminesmith@win_5.com?name.first=NewNameAgain')
+ .put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
.set(constants.nonSecretariatUserHeaders)
+ .send({
+ ...user,
+ name: {
+ ...user.name,
+ first: 'NewNameAgain'
+ }
+ })
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
@@ -39,13 +48,19 @@ describe('Testing Edit user endpoint', () => {
})
})
it('Should return an error when admin is required with registry enabled', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body })
await chai.request(app)
- .put('/api/registry/org/win_5/user/jasminesmith@win_5.com?new_username=NewUsername')
+ .put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
.set(constants.nonSecretariatUserHeaders)
+ .send({
+ ...user,
+ username: 'NewUsername'
+ })
.then((res, err) => {
expect(err).to.be.undefined
- expect(res).to.have.status(403)
- expect(res.body.error).to.contain('NOT_ORG_ADMIN_OR_SECRETARIAT_UPDATE')
+ expect(res).to.have.status(400)
+ expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD')
})
})
it('Should not allow a first name of more than 100 characters', async () => {
@@ -58,12 +73,22 @@ describe('Testing Edit user endpoint', () => {
})
})
it('Should not allow a first name of more than 100 characters with registry enabled', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body })
await chai.request(app)
- .put('/api/registry/org/win_5/user/jasminesmith@win_5.com?name.first=1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567')
+ .put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
.set(constants.nonSecretariatUserHeaders)
+ .send({
+ ...user,
+ name: {
+ ...user.name,
+ first: '1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567'
+ }
+ })
.then((res, err) => {
expect(res).to.have.status(400)
- expect(res.body.error).to.contain('BAD_INPUT')
+ expect(res.body.errors).to.have.lengthOf(1)
+ expect(res.body.errors[0].message).to.contain('must NOT have more than 100 characters')
})
})
it('Should not allow a middle name of more than 100 characters', async () => {
@@ -76,12 +101,22 @@ describe('Testing Edit user endpoint', () => {
})
})
it('Should not allow a middle name of more than 100 characters with registry enabled', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body })
await chai.request(app)
- .put('/api/registry/org/win_5/user/jasminesmith@win_5.com?name.middle=1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567')
+ .put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
.set(constants.nonSecretariatUserHeaders)
+ .send({
+ ...user,
+ name: {
+ ...user.name,
+ middle: '1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567'
+ }
+ })
.then((res, err) => {
expect(res).to.have.status(400)
- expect(res.body.error).to.contain('BAD_INPUT')
+ expect(res.body.errors).to.have.lengthOf(1)
+ expect(res.body.errors[0].message).to.contain('must NOT have more than 100 characters')
})
})
it('Should not allow a last name of more than 100 characters', async () => {
@@ -94,12 +129,22 @@ describe('Testing Edit user endpoint', () => {
})
})
it('Should not allow a last name of more than 100 characters with registry enabled', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body })
await chai.request(app)
- .put('/api/registry/org/win_5/user/jasminesmith@win_5.com?name.last=1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567')
+ .put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
.set(constants.nonSecretariatUserHeaders)
+ .send({
+ ...user,
+ name: {
+ ...user.name,
+ last: '1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567'
+ }
+ })
.then((res, err) => {
expect(res).to.have.status(400)
- expect(res.body.error).to.contain('BAD_INPUT')
+ expect(res.body.errors).to.have.lengthOf(1)
+ expect(res.body.errors[0].message).to.contain('must NOT have more than 100 characters')
})
})
it('Should not allow a suffix of more than 100 characters', async () => {
@@ -112,21 +157,33 @@ describe('Testing Edit user endpoint', () => {
})
})
it('Should not allow a suffix of more than 100 characters with registry enabled', async () => {
+ let user
+ await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body })
await chai.request(app)
.put('/api/registry/org/win_5/user/jasminesmith@win_5.com?name.suffix=1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567')
.set(constants.nonSecretariatUserHeaders)
+ .send({
+ ...user,
+ name: {
+ ...user.name,
+ suffix: '1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567'
+ }
+ })
.then((res, err) => {
expect(res).to.have.status(400)
- expect(res.body.error).to.contain('BAD_INPUT')
+ expect(res.body.errors).to.have.lengthOf(1)
+ expect(res.body.errors[0].message).to.contain('must NOT have more than 100 characters')
})
})
it('expect error when trying to add existing user to the same org', async () => {
const user = constants.nonSecretariatUserHeaders3['CVE-API-USER']
const org = constants.nonSecretariatUserHeaders3['CVE-API-ORG']
await chai.request(app)
- .put(`/api/registry/org/${org}/user/${user}?org_short_name=${org}`)
+ .put(`/api/registry/org/${org}/user/${user}`)
.set(constants.headers)
- .send()
+ .send({
+ org_short_name: org
+ })
.then((res) => {
expect(res).to.have.status(403)
expect(res.body.error).to.contain('USER_ALREADY_IN_ORG')
diff --git a/test/unit-tests/org/orgCreateADPTest.js b/test/unit-tests/org/orgCreateADPTest.js
index 031885474..51c352890 100644
--- a/test/unit-tests/org/orgCreateADPTest.js
+++ b/test/unit-tests/org/orgCreateADPTest.js
@@ -106,30 +106,4 @@ describe('Testing creating orgs with the ADP role', () => {
expect(mockSession.commitTransaction.calledOnce).to.be.true
expect(mockSession.endSession.calledOnce).to.be.true
})
-
- // This is OBE
- it.skip('Should have nonzero id_quota when created with ADP and CNA role', async () => {
- const req = {
- ctx: {
- uuid: faker.datatype.uuid(),
- repositories: {
- getBaseOrgRepository,
- getBaseUserRepository
- },
- body: {
- ...stubAdpCnaOrg
- }
- },
- query: {
- registry: 'false' // query parameters are strings
- }
- }
-
- await ORG_CREATE_SINGLE(req, res, next)
-
- expect(status.args[0][0]).to.equal(200)
- expect(updateOrg.args[0][1].policies.id_quota).to.equal(200)
- expect(updateOrg.args[0][1].authority.active_roles[0]).to.equal('ADP')
- expect(updateOrg.args[0][1].authority.active_roles[1]).to.equal('CNA')
- })
})
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 fb7202807..ba9a48eee 100644
--- a/test/unit-tests/review-object/review-object.controller.test.js
+++ b/test/unit-tests/review-object/review-object.controller.test.js
@@ -2,19 +2,39 @@
/* eslint-disable no-unused-expressions */
const { expect } = require('chai')
const sinon = require('sinon')
+const mongoose = require('mongoose')
const controller = require('../../../src/controller/review-object.controller/review-object.controller.js')
describe('Review Object Controller', function () {
- let req, res, next, repoStub, orgRepoStub
+ let req, res, next, repoStub, orgRepoStub, userRepoStub, sessionStub
beforeEach(() => {
- repoStub = { }
- orgRepoStub = { }
+ repoStub = {}
+ orgRepoStub = {}
+ userRepoStub = {}
+
+ // Mock mongoose session
+ sessionStub = {
+ startTransaction: sinon.stub(),
+ commitTransaction: sinon.stub().resolves(),
+ abortTransaction: sinon.stub().resolves(),
+ endSession: sinon.stub().resolves()
+ }
+ sinon.stub(mongoose, 'startSession').resolves(sessionStub)
req = {
params: {},
body: {},
- ctx: { repositories: { getReviewObjectRepository: () => repoStub, getBaseOrgRepository: () => orgRepoStub } }
+ query: {},
+ ctx: {
+ org: 'mitre',
+ user: 'test_user@mitre.org',
+ repositories: {
+ getReviewObjectRepository: () => repoStub,
+ getBaseOrgRepository: () => orgRepoStub,
+ getBaseUserRepository: () => userRepoStub
+ }
+ }
}
res = {
@@ -26,6 +46,10 @@ describe('Review Object Controller', function () {
orgRepoStub.isSecretariatByShortName = sinon.stub().resolves(true)
})
+ afterEach(() => {
+ sinon.restore()
+ })
+
describe('getReviewObjectByOrgIdentifier', function () {
it('should return 400 if identifier is missing', async () => {
await controller.getReviewObjectByOrgIdentifier(req, res, next)
@@ -52,17 +76,85 @@ describe('Review Object Controller', function () {
expect(res.status.calledWith(200)).to.be.true
expect(res.json.calledWith({ name: short })).to.be.true
})
+
+ it('should return 404 if no pending review object exists for UUID', async () => {
+ const uuid = '123e4567-e89b-12d3-a456-426614174000'
+ req.params.identifier = uuid
+ repoStub.getOrgReviewObjectByOrgUUID = sinon.stub().resolves(null)
+ await controller.getReviewObjectByOrgIdentifier(req, res, next)
+ expect(res.status.calledWith(404)).to.be.true
+ expect(res.json.calledWith({ message: 'No pending review object exists for this organization' })).to.be.true
+ })
+
+ it('should return 404 if no pending review object exists for short_name', async () => {
+ const short = 'myorg'
+ req.params.identifier = short
+ repoStub.getOrgReviewObjectByOrgShortname = sinon.stub().resolves(null)
+ await controller.getReviewObjectByOrgIdentifier(req, res, next)
+ expect(res.status.calledWith(404)).to.be.true
+ expect(res.json.calledWith({ message: 'No pending review object exists for this organization' })).to.be.true
+ })
+ })
+
+ describe('getReviewObjectByUUID', function () {
+ it('should return review object when found', async () => {
+ const uuid = 'review-uuid-123'
+ const reviewObj = { uuid, target_object_uuid: 'org-uuid', new_review_data: { short_name: 'test' } }
+ req.params.uuid = uuid
+ repoStub.findOneByUUIDWithConversation = sinon.stub().resolves(reviewObj)
+ await controller.getReviewObjectByUUID(req, res, next)
+ expect(repoStub.findOneByUUIDWithConversation.calledWith(uuid, true)).to.be.true
+ expect(res.status.calledWith(200)).to.be.true
+ expect(res.json.calledWith(reviewObj)).to.be.true
+ })
+
+ it('should pass isSecretariat=false for non-secretariat users', async () => {
+ const uuid = 'review-uuid-123'
+ req.params.uuid = uuid
+ orgRepoStub.isSecretariatByShortName = sinon.stub().resolves(false)
+ repoStub.findOneByUUIDWithConversation = sinon.stub().resolves(null)
+ await controller.getReviewObjectByUUID(req, res, next)
+ expect(repoStub.findOneByUUIDWithConversation.calledWith(uuid, false)).to.be.true
+ })
+
+ it('should return null when review object not found', async () => {
+ const uuid = 'nonexistent-uuid'
+ req.params.uuid = uuid
+ repoStub.findOneByUUIDWithConversation = sinon.stub().resolves(null)
+ await controller.getReviewObjectByUUID(req, res, next)
+ expect(res.status.calledWith(200)).to.be.true
+ expect(res.json.calledWith(null)).to.be.true
+ })
})
describe('getAllReviewObjects', function () {
it('should return all review objects', async () => {
- const data = [{ id: 1 }, { id: 2 }]
- repoStub.getAllReviewObjects = sinon.stub().resolves(data)
+ const data = { reviewObjects: [{ id: 1 }, { id: 2 }], totalDocs: 2 }
+ repoStub.getAllReviewObjectsPaginated = sinon.stub().resolves(data)
await controller.getAllReviewObjects(req, res, next)
- expect(repoStub.getAllReviewObjects.calledOnce).to.be.true
+ expect(repoStub.getAllReviewObjectsPaginated.calledOnce).to.be.true
expect(res.status.calledWith(200)).to.be.true
expect(res.json.calledWith(data)).to.be.true
})
+
+ it('should pass page parameter when provided', async () => {
+ const data = { reviewObjects: [{ id: 3 }], totalDocs: 5 }
+ req.query.page = '2'
+ repoStub.getAllReviewObjectsPaginated = sinon.stub().resolves(data)
+ await controller.getAllReviewObjects(req, res, next)
+ expect(res.status.calledWith(200)).to.be.true
+ const callArgs = repoStub.getAllReviewObjectsPaginated.getCall(0).args
+ expect(callArgs[0].page).to.equal(2)
+ })
+
+ it('should pass status filter when provided', async () => {
+ const data = { reviewObjects: [], totalDocs: 0 }
+ req.query.status = 'pending'
+ repoStub.getAllReviewObjectsPaginated = sinon.stub().resolves(data)
+ await controller.getAllReviewObjects(req, res, next)
+ const callArgs = repoStub.getAllReviewObjectsPaginated.getCall(0).args
+ expect(callArgs[1]).to.equal('pending')
+ })
})
describe('updateReviewObjectByReviewUUID', function () {
@@ -117,4 +209,141 @@ describe('Review Object Controller', function () {
expect(res.json.calledWith(created)).to.be.true
})
})
+
+ describe('approveReviewObject', function () {
+ const reviewUUID = 'review-uuid-123'
+ const orgUUID = 'org-uuid-456'
+ const reviewObject = {
+ uuid: reviewUUID,
+ target_object_uuid: orgUUID,
+ new_review_data: { short_name: 'updated_org' }
+ }
+ const orgObj = {
+ short_name: 'original_org',
+ toObject: () => ({ short_name: 'original_org' })
+ }
+ const updatedOrgObj = {
+ short_name: 'updated_org',
+ toObject: () => ({ short_name: 'updated_org' })
+ }
+
+ beforeEach(() => {
+ req.params.uuid = reviewUUID
+ req.body = {}
+ })
+
+ it('should return 404 if review object not found', async () => {
+ repoStub.findOneByUUID = sinon.stub().resolves(null)
+ await controller.approveReviewObject(req, res, next)
+ expect(res.status.calledWith(404)).to.be.true
+ expect(res.json.calledWith({ message: `No review object found with UUID ${reviewUUID}` })).to.be.true
+ expect(sessionStub.abortTransaction.calledOnce).to.be.true
+ })
+
+ it('should return 404 if organization not found', async () => {
+ repoStub.findOneByUUID = sinon.stub().resolves(reviewObject)
+ orgRepoStub.findOneByUUID = sinon.stub().resolves(null)
+ await controller.approveReviewObject(req, res, next)
+ expect(res.status.calledWith(404)).to.be.true
+ expect(res.json.calledWith({ message: 'Organization not found for this review object' })).to.be.true
+ expect(sessionStub.abortTransaction.calledOnce).to.be.true
+ })
+
+ it('should approve review object and update organization with review data', async () => {
+ repoStub.findOneByUUID = sinon.stub().resolves(reviewObject)
+ orgRepoStub.findOneByUUID = sinon.stub()
+ .onFirstCall().resolves(orgObj)
+ .onSecondCall().resolves(updatedOrgObj)
+ repoStub.approveReviewOrgObject = sinon.stub().resolves({ ...reviewObject, status: 'approved' })
+ orgRepoStub.updateOrgFull = sinon.stub().resolves(updatedOrgObj)
+ userRepoStub.getUserUUID = sinon.stub().resolves('user-uuid')
+
+ await controller.approveReviewObject(req, res, next)
+ expect(orgRepoStub.updateOrgFull.calledOnce).to.be.true
+ expect(res.status.calledWith(200)).to.be.true
+ expect(res.json.calledWith({ short_name: 'updated_org' })).to.be.true
+ })
+ })
+
+ describe('rejectReviewObject', function () {
+ const reviewUUID = 'review-uuid-123'
+
+ beforeEach(() => {
+ req.params.uuid = reviewUUID
+ })
+
+ it('should return 404 if review object not found', async () => {
+ repoStub.rejectReviewOrgObject = sinon.stub().resolves(null)
+ await controller.rejectReviewObject(req, res, next)
+ expect(res.status.calledWith(404)).to.be.true
+ expect(res.json.calledWith({ message: `No review object found with UUID ${reviewUUID}` })).to.be.true
+ })
+
+ it('should return 200 with rejected review object', async () => {
+ const rejectedObj = { uuid: reviewUUID, status: 'rejected' }
+ repoStub.rejectReviewOrgObject = sinon.stub().resolves(rejectedObj)
+ await controller.rejectReviewObject(req, res, next)
+ expect(repoStub.rejectReviewOrgObject.calledWith(reviewUUID)).to.be.true
+ expect(sessionStub.commitTransaction.calledOnce).to.be.true
+ expect(res.status.calledWith(200)).to.be.true
+ expect(res.json.calledWith(rejectedObj)).to.be.true
+ })
+ })
+
+ describe('getReviewHistoryByOrgShortNamePaginated', function () {
+ const orgShortName = 'test_org'
+
+ beforeEach(() => {
+ req.params.identifier = orgShortName
+ })
+
+ it('should return 404 if organization does not exist', async () => {
+ orgRepoStub.orgExists = sinon.stub().resolves(false)
+ await controller.getReviewHistoryByOrgShortNamePaginated(req, res, next)
+ expect(res.status.calledWith(404)).to.be.true
+ })
+
+ it('should return paginated review history', async () => {
+ const historyData = {
+ reviewObjects: [
+ { uuid: 'rev-1', status: 'approved' },
+ { uuid: 'rev-2', status: 'rejected' }
+ ],
+ totalDocs: 2
+ }
+ orgRepoStub.orgExists = sinon.stub().resolves(true)
+ repoStub.getReviewHistoryByOrgShortNamePaginated = sinon.stub().resolves(historyData)
+ await controller.getReviewHistoryByOrgShortNamePaginated(req, res, next)
+ expect(res.status.calledWith(200)).to.be.true
+ expect(res.json.calledWith(historyData)).to.be.true
+ })
+
+ it('should pass page parameter when provided', async () => {
+ req.query.page = '3'
+ orgRepoStub.orgExists = sinon.stub().resolves(true)
+ repoStub.getReviewHistoryByOrgShortNamePaginated = sinon.stub().resolves({ reviewObjects: [], totalDocs: 0 })
+ await controller.getReviewHistoryByOrgShortNamePaginated(req, res, next)
+ const callArgs = repoStub.getReviewHistoryByOrgShortNamePaginated.getCall(0).args
+ expect(callArgs[1].page).to.equal(3) // second argument is options with page
+ })
+
+ it('should pass include_conversations parameter when provided', async () => {
+ req.query.include_conversations = 'true'
+ orgRepoStub.orgExists = sinon.stub().resolves(true)
+ repoStub.getReviewHistoryByOrgShortNamePaginated = sinon.stub().resolves({ reviewObjects: [], totalDocs: 0 })
+ await controller.getReviewHistoryByOrgShortNamePaginated(req, res, next)
+ // Verify that includeConversations argument is true
+ const callArgs = repoStub.getReviewHistoryByOrgShortNamePaginated.getCall(0).args // first call
+ expect(callArgs[2]).to.equal('true') // third argument is includeConversations
+ })
+
+ it('should pass isSecretariat flag to repository', async () => {
+ orgRepoStub.orgExists = sinon.stub().resolves(true)
+ orgRepoStub.isSecretariatByShortName = sinon.stub().resolves(false)
+ repoStub.getReviewHistoryByOrgShortNamePaginated = sinon.stub().resolves({ reviewObjects: [], totalDocs: 0 })
+ await controller.getReviewHistoryByOrgShortNamePaginated(req, res, next)
+ const callArgs = repoStub.getReviewHistoryByOrgShortNamePaginated.getCall(0).args
+ expect(callArgs[3]).to.equal(false)
+ })
+ })
})