diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 8f4ed585e..a7dbaa6c2 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -24,6 +24,13 @@ "description": "
All registered users can access this endpoint
Regular, CNA & Admin Users: Retrieves filtered CVE IDs owned by the user's organization
Secretariat: Retrieves filtered CVE IDs owned by any organization
", "operationId": "cveIdGetFiltered", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/cveIdGetFilteredState" }, @@ -126,6 +133,13 @@ "description": "User must belong to an organization with the CNA or Secretariat role
CNA: Reserves CVE IDs for the CNA
Secretariat: Reserves CVE IDs for any organization
", "operationId": "cveIdReserve", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/amount" }, @@ -522,6 +536,13 @@ }, "description": "The id of the CVE ID to update" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/org" }, @@ -620,6 +641,13 @@ }, "description": "The year of the CVE-ID-Range" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -905,6 +933,13 @@ }, "description": "The CVE ID for the record being submitted" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1005,6 +1040,13 @@ }, "description": "The CVE ID for the record being updated" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1098,6 +1140,13 @@ "description": "User must belong to an organization with the Secretariat role
Secretariat: Retrieves all CVE records for all organizations
", "operationId": "cveGetFiltered", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/cveRecordFilteredTimeModifiedLt" }, @@ -1256,6 +1305,13 @@ "description": "User must belong to an organization with the Secretariat role
Secretariat: Retrieves all CVE records for all organizations
", "operationId": "cveGetFilteredCursor", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/cveRecordFilteredTimeModifiedLt" }, @@ -1372,6 +1428,13 @@ }, "description": "The CVE ID for the record being created" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1484,6 +1547,13 @@ }, "description": "The CVE ID for which the record is being updated" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1598,6 +1668,13 @@ }, "description": "The CVE ID for the record being rejected" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1699,6 +1776,13 @@ }, "description": "The CVE ID for the record being rejected" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1802,6 +1886,13 @@ }, "description": "The CVE ID for which the record is being updated" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1896,6 +1987,13 @@ "description": "User must belong to an organization with the Secretariat role
Secretariat: Retrieves information about all organizations
", "operationId": "orgAll", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/pageQuery" }, @@ -1980,6 +2078,13 @@ "description": "User must belong to an organization with the Secretariat role
Secretariat: Creates an organization
", "operationId": "orgCreateSingle", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2082,6 +2187,13 @@ }, "description": "The shortname or UUID of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2153,6 +2265,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2174,6 +2296,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/id_quota" }, @@ -2260,6 +2389,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2281,6 +2420,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2352,6 +2498,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2373,6 +2529,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/pageQuery" }, @@ -2447,6 +2610,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2468,6 +2641,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2579,6 +2759,13 @@ }, "description": "The username of the user" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2650,6 +2837,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } }, "put": { @@ -2678,6 +2875,13 @@ }, "description": "The username of the user" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/active" }, @@ -2776,6 +2980,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2806,6 +3020,13 @@ }, "description": "The username of the user" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2877,6 +3098,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2889,6 +3120,13 @@ "description": "User must belong to an organization with the Secretariat role
Secretariat: Retrieves information about all users for all organizations
", "operationId": "userAll", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/pageQuery" }, @@ -2974,13 +3212,1136 @@ "summary": "Checks that the system is running (accessible to all users)", "description": "Endpoint is accessible to all
Returns a 200 response code when CVE Services are running
", "operationId": "healthCheck", - "parameters": [], "responses": { "200": { "description": "Returns a 200 response code" } } } + }, + "/registryOrg": { + "get": { + "tags": [ + "Registry Organization" + ], + "summary": "Retrieves information about all registry organizations (accessible to Secretariat only)", + "description": "Only users with Secretariat role can access this endpoint
Secretariat: Retrieves a list of all registry organizations
", + "operationId": "getAllRegistryOrgs", + "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/pageQuery" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "A list of all registry organizations, along with pagination fields if results span multiple pages of data", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-org/get-registry-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" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + }, + "post": { + "tags": [ + "Registry Organization" + ], + "summary": "Creates a new registry organization (accessible to Secretariat only)", + "description": "Only users with Secretariat role can access this endpoint
Secretariat: Creates a new registry organization
", + "operationId": "createRegistryOrg", + "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "201": { + "description": "The registry organization was successfully created", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-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" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgPayload" + } + } + } + } + } + }, + "/registryOrg/{identifier}": { + "get": { + "tags": [ + "Registry Organization" + ], + "summary": "Retrieves information about a specific registry organization", + "description": "All authenticated users can access this endpoint
All Users: Retrieves information about the specified registry organization
", + "operationId": "getSingleRegistryOrg", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry organization" + }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The requested registry organization information is returned", + "content": { + "application/json": { + "schema": { + "$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" + } + } + } + }, + "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" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Registry Organization" + ], + "summary": "Deletes an existing registry organization (accessible to Secretariat only)", + "description": "Only users with Secretariat role can access this endpoint
Secretariat: Deletes an existing registry organization
", + "operationId": "deleteRegistryOrg", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry organization to delete" + }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The registry organization was successfully deleted", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/delete-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" + } + } + } + } + } + } + }, + "/registryOrg/{shortname}": { + "put": { + "tags": [ + "Registry Organization" + ], + "summary": "Updates an existing registry organization (accessible to Secretariat only)", + "description": "Only users with Secretariat role can access this endpoint
Secretariat: Updates an existing registry organization
", + "operationId": "updateRegistryOrg", + "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The Shortname of the registry organization to update" + }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The registry organization was successfully 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" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgPayload" + } + } + } + } + } + }, + "/registryOrg/{shortname}/users": { + "get": { + "tags": [ + "Registry User" + ], + "summary": "Retrieves all users for the organization with the specified short name (accessible to all registered users)", + "description": "All registered users can access this endpoint
Regular, CNA & Admin Users: Retrieves information about users in the same organization
Secretariat: Retrieves all user information for any organization
", + "operationId": "registryUserOrgAll", + "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the organization" + }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/pageQuery" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Returns all users for the organization, along with pagination fields if results span multiple pages of data", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/list-users-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" + } + } + } + } + } + } + }, + "/registryOrg/{shortname}/user": { + "post": { + "tags": [ + "Registry User" + ], + "summary": "Create a user with the provided short name as the owning organization (accessible to Admins and Secretariats)", + "description": "User must belong to an organization with the Secretariat role or be an Admin of the organization
Admin User: Creates a user for the Admin's organization
Secretariat: Creates a user for any organization
", + "operationId": "RegistryUserCreateSingle", + "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 new user information (with the secret)", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/create-user-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" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/create-user-request.json" + } + } + } + } + } + }, + "/registryUser": { + "get": { + "tags": [ + "Registry User" + ], + "summary": "Retrieves information about all registry users (accessible to Secretariat only)", + "description": "Only users with Secretariat role can access this endpoint
Secretariat: Retrieves a list of all registry users
", + "operationId": "getAllRegistryUsers", + "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/pageQuery" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "A list of all registry organizations, along with pagination fields if results span multiple pages of data", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-user/get-registry-users-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" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + }, + "post": { + "tags": [ + "Registry User" + ], + "summary": "Creates a new registry user (accessible to Secretariat only)", + "description": "Only users with Secretariat role can access this endpoint
Secretariat: Creates a new registry user
", + "operationId": "createRegistryUser", + "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "201": { + "description": "The registry user was successfully created", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/create-user-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" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserPayload" + } + } + } + } + } + }, + "/registryUser/{identifier}": { + "get": { + "tags": [ + "Registry User" + ], + "summary": "Retrieves information about a specific registry user", + "description": "All authenticated users can access this endpoint
All Users: Retrieves information about the specified registry user
", + "operationId": "getSingleRegistryUser", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry user" + }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The requested registry user information is returned", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/get-user-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" + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Registry User" + ], + "summary": "Updates an existing registry user (accessible to Secretariat only)", + "description": "Only users with Secretariat role can access this endpoint
Secretariat: Updates an existing registry user
", + "operationId": "updateRegistryUser", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry user to update" + }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The registry user was successfully updated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/update-user-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" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserPayload" + } + } + } + } + }, + "delete": { + "tags": [ + "Registry User" + ], + "summary": "Deletes an existing registry user (accessible to Secretariat only)", + "description": "Only users with Secretariat role can access this endpoint
Secretariat: Deletes an existing registry user
", + "operationId": "deleteRegistryUser", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry user to delete" + }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The registry user was successfully deleted", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/delete-user-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" + } + } + } + } + } + } } }, "components": { diff --git a/datadump/pre-population/registry-orgs.json b/datadump/pre-population/registry-orgs.json new file mode 100644 index 000000000..b3a8d03b9 --- /dev/null +++ b/datadump/pre-population/registry-orgs.json @@ -0,0 +1,147 @@ +[ + { + "UUID": "org-secretariat", + "long_name": "Secretariat Org", + "short_name": "SecretariatOrg", + "aliases": [], + "cve_program_org_function": "Secretariat", + "authority": { + "active_roles": [ + "CNA", + "Top Level Root", + "CNA-LR", + "Bulk Download", + "SECRETARIAT" + ] + }, + "reports_to": null, + "oversees": ["org-uuid-1", "org-uuid-3"], + "root_or_tlr": true, + "users": ["user-uuid-secretariat"], + "charter_or_scope": "All Things CVE", + "disclosure_policy": "When the time is right", + "product_list": "Product A, Product B, Product C", + "soft_quota": 100, + "hard_quota": 150, + "contact_info": { + "additional_contact_users": [], + "poc": "John Doe", + "poc_email": "john.doe@secretariat.com", + "poc_phone": "+1-555-001-1001", + "admins": [], + "org_email": "contact@secretariat.com", + "website": "https://www.cve.org" + }, + "in_use": true, + "created": "2023-06-01T00:00:00.000Z", + "last_updated": "2023-06-01T00:00:00.000Z" + }, + { + "UUID": "org-uuid-1", + "long_name": "Test Organization One", + "short_name": "TestOrg1", + "aliases": [ + "TO1", + "Test1" + ], + "cve_program_org_function": "CNA", + "authority": { + "active_roles": [ + "CNA" + ] + }, + "reports_to": null, + "oversees": ["org-uuid-2"], + "root_or_tlr": true, + "users": ["user-uuid-1"], + "charter_or_scope": "Responsible for technology sector vulnerabilities", + "disclosure_policy": "90-day disclosure policy", + "product_list": "Product A, Product B, Product C", + "soft_quota": 100, + "hard_quota": 150, + "contact_info": { + "additional_contact_users": [], + "poc": "John Doe", + "poc_email": "john.doe@testorg1.com", + "poc_phone": "+1-555-001-1001", + "admins": ["user-uuid-1"], + "org_email": "contact@testorg1.com", + "website": "https://www.testorg1.com" + }, + "in_use": true, + "created": "2023-06-01T00:00:00.000Z", + "last_updated": "2023-06-01T00:00:00.000Z" + }, + { + "UUID": "org-uuid-2", + "long_name": "Security Solutions Inc.", + "short_name": "SecSol", + "aliases": [ + "SSI", + "SecInc" + ], + "cve_program_org_function": "CNA", + "authority": { + "active_roles": [ + "CNA" + ] + }, + "reports_to": "org-uuid-1", + "oversees": [], + "root_or_tlr": true, + "users": ["user-uuid-2"], + "charter_or_scope": "Focused on cybersecurity software vulnerabilities", + "disclosure_policy": "60-day responsible disclosure policy", + "product_list": "SecureShield, CyberGuard, DataDefender", + "soft_quota": 75, + "hard_quota": 100, + "contact_info": { + "additional_contact_users": [], + "poc": "Jane Smith", + "poc_email": "jane.smith@secsol.com", + "poc_phone": "+1-555-002-2002", + "admins": ["user-uuid-2"], + "org_email": "info@secsol.com", + "website": "https://www.secsol.com" + }, + "in_use": true, + "created": "2023-06-02T00:00:00.000Z", + "last_updated": "2023-06-02T00:00:00.000Z" + }, + { + "UUID": "org-uuid-3", + "long_name": "Global Network Systems", + "short_name": "GNS", + "aliases": [ + "GlobalNet", + "NetSys" + ], + "cve_program_org_function": "CNA", + "authority": { + "active_roles": [ + "CNA" + ] + }, + "reports_to": null, + "oversees": [], + "root_or_tlr": false, + "users": ["user-uuid-3"], + "charter_or_scope": "Specializing in network infrastructure vulnerabilities", + "disclosure_policy": "45-day coordinated disclosure policy", + "product_list": "NetRouter, CloudConnect, SecureSwitch", + "soft_quota": 120, + "hard_quota": 180, + "contact_info": { + "additional_contact_users": [], + "poc": "Michael Johnson", + "poc_email": "michael.johnson@gns.com", + "poc_phone": "+1-555-003-3003", + "admins": ["user-uuid-3"], + "org_email": "contact@gns.com", + "website": "https://www.gns.com" + }, + "in_use": true, + "created": "2023-06-03T00:00:00.000Z", + "last_updated": "2023-06-03T00:00:00.000Z" + } +] \ No newline at end of file diff --git a/datadump/pre-population/registry-users.json b/datadump/pre-population/registry-users.json new file mode 100644 index 000000000..636022c8c --- /dev/null +++ b/datadump/pre-population/registry-users.json @@ -0,0 +1,114 @@ +[ + { + "UUID": "user-uuid-secretariat", + "user_id": "secretariat", + "name": { + "first": "David", + "last": "Rocca", + "middle": "T", + "suffix": "" + }, + "org_affiliations": [ + { + "org_id": "org-secretariat", + "email": "drocca@mitre.org", + "phone": "+1-555-001-1001" + } + ], + "cve_program_org_membership": [ + { + "program_org": "org-secretariat", + "role": "Admin", + "status": "active" + } + ], + "created": "2023-06-01T00:00:00.000Z", + "created_by": "drocca", + "last_updated": "2023-06-01T00:00:00.000Z", + "last_active": "2023-06-01T00:00:00.000Z" + }, + { + "UUID": "user-uuid-1", + "user_id": "user1@testorg1.com", + "name": { + "first": "John", + "last": "Doe", + "middle": "A", + "suffix": "Jr" + }, + "org_affiliations": [ + { + "org_id": "org-uuid-1", + "email": "john.doe@testorg1.com", + "phone": "+1-555-001-1001" + } + ], + "cve_program_org_membership": [ + { + "program_org": "org-uuid-1", + "role": "Admin", + "status": "active" + } + ], + "created": "2023-06-01T00:00:00.000Z", + "created_by": "system", + "last_updated": "2023-06-01T00:00:00.000Z", + "last_active": "2023-06-01T00:00:00.000Z" + }, + { + "UUID": "user-uuid-2", + "user_id": "jane.smith@secsol.com", + "name": { + "first": "Jane", + "last": "Smith", + "middle": "B", + "suffix": "" + }, + "org_affiliations": [ + { + "org_id": "org-uuid-2", + "email": "jane.smith@secsol.com", + "phone": "+1-555-002-2002" + } + ], + "cve_program_org_membership": [ + { + "program_org": "org-uuid-2", + "role": "Admin", + "status": "active" + } + ], + "created": "2023-06-02T00:00:00.000Z", + "created_by": "system", + "last_updated": "2023-06-02T00:00:00.000Z", + "last_active": "2023-06-02T00:00:00.000Z" + }, + { + "UUID": "user-uuid-3", + "user_id": "michael.johnson@gns.com", + "name": { + "first": "Michael", + "last": "Johnson", + "middle": "C", + "suffix": "" + }, + "org_affiliations": [ + { + "org_id": "org-uuid-3", + "email": "michael.johnson@gns.com", + "phone": "+1-555-003-3003" + } + ], + "cve_program_org_membership": [ + { + "program_org": "org-uuid-3", + "role": "Admin", + "status": "active" + } + ], + "created": "2023-06-03T00:00:00.000Z", + "created_by": "system", + "last_updated": "2023-06-03T00:00:00.000Z", + "last_active": "2023-06-03T00:00:00.000Z" + } +] \ No newline at end of file diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json new file mode 100644 index 000000000..839f24d92 --- /dev/null +++ b/schemas/registry-org/get-registry-org-response.json @@ -0,0 +1,157 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cve.mitre.org/schema/org/organization.json", + "type": "object", + "title": "CVE Organization", + "description": "JSON Schema for CVE Organization data", + "properties": { + "UUID": { + "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" + }, + "aliases": { + "type": "array", + "items": { + "type": "string" + }, + "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"] + } + } + }, + "required": ["active_roles"] + }, + "reports_to": { + "type": ["string", "null"], + "description": "UUID of the parent organization, if any" + }, + "oversees": { + "type": "array", + "items": { + "type": "string" + }, + "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" + }, + "hard_quota": { + "type": "integer", + "description": "Hard quota for CVE IDs" + }, + "contact_info": { + "type": "object", + "properties": { + "additional_contact_users": { + "type": "array", + "items": { + "type": "string" + } + }, + "poc": { + "type": "string", + "description": "Point of contact name" + }, + "poc_email": { + "type": "string", + "format": "email", + "description": "Point of contact email" + }, + "poc_phone": { + "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", + "description": "Organization's email address" + }, + "website": { + "type": "string", + "format": "uri", + "description": "Organization's website URL" + } + }, + "required": ["poc", "poc_email", "admins", "org_email"] + }, + "in_use": { + "type": "boolean", + "description": "Indicates if the organization is currently active" + }, + "created": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the organization was created" + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "Timestamp of the last update to the organization data" + } + }, + "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-user/get-registry-users-response.json b/schemas/registry-user/get-registry-users-response.json new file mode 100644 index 000000000..d5df13284 --- /dev/null +++ b/schemas/registry-user/get-registry-users-response.json @@ -0,0 +1,136 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cve.mitre.org/schema/user/registry-user.json", + "type": "object", + "title": "CVE Registry User", + "description": "JSON Schema for CVE Registry User data", + "properties": { + "UUID": { + "type": "string", + "description": "Unique identifier for the user" + }, + "user_id": { + "type": "string", + "description": "User's identifier or username" + }, + "name": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "User's first name" + }, + "last": { + "type": "string", + "description": "User's last name" + }, + "middle": { + "type": "string", + "description": "User's middle name" + }, + "suffix": { + "type": "string", + "description": "User's name suffix" + } + }, + "required": [ + "first", + "last" + ] + }, + "org_affiliations": { + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of organizations the user is affiliated with" + }, + "cve_program_org_membership": { + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of CVE program organizations the user is a member of" + }, + "authority": { + "type": "object", + "properties": { + "active_roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ADMIN", + "PUBLISHER" + ] + } + } + }, + "required": [ + "active_roles" + ] + }, + "secret": { + "type": "string", + "description": "Hashed secret for user authentication" + }, + "last_active": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp of the user's last activity" + }, + "deactivation_date": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp of when the user was deactivated, if applicable" + }, + "contact_info": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "User's email address" + }, + "phone": { + "type": "string", + "description": "User's phone number" + } + }, + "required": [ + "email" + ] + }, + "in_use": { + "type": "boolean", + "description": "Indicates if the user account is currently active" + }, + "created": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the user account was created" + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "Timestamp of the last update to the user data" + } + }, + "required": [ + "UUID", + "user_id", + "name", + "authority", + "secret", + "contact_info", + "in_use", + "created", + "last_updated" + ] +} \ No newline at end of file diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 15a160a62..fd839e984 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -4,7 +4,7 @@ const mw = require('../../middleware/middleware') const errorMsgs = require('../../middleware/errorMessages') const controller = require('./org.controller') const { body, param, query } = require('express-validator') -const { parseGetParams, parsePostParams, parseError, isOrgRole, isUserRole, isValidUsername } = require('./org.middleware') +const { parseGetParams, parsePostParams, parseError, isOrgRole, isUserRole, isValidUsername, validateCreateOrgParameters, validateUpdateOrgParameters } = require('./org.middleware') const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const getConstants = require('../../../src/constants').getConstants const CONSTANTS = getConstants() @@ -76,12 +76,14 @@ router.get('/org', */ mw.validateUser, mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + param(['registry']).optional().isBoolean(), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page', 'registry']) }), query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), parseError, parseGetParams, controller.ORG_ALL) + router.post('/org', /* #swagger.tags = ['Organization'] @@ -155,15 +157,10 @@ router.post('/org', } } */ + param(['registry']).optional().isBoolean(), mw.validateUser, mw.onlySecretariat, - body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - body(['name']).isString().trim().notEmpty(), - body(['authority.active_roles']).optional() - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole), - body(['policies.id_quota']).optional().not().isArray().isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }).withMessage(errorMsgs.ID_QUOTA), + validateCreateOrgParameters(), parseError, parsePostParams, controller.ORG_CREATE_SINGLE) @@ -234,6 +231,7 @@ router.get('/org/:identifier', } */ mw.validateUser, + param(['registry']).optional().isBoolean(), param(['identifier']).isString().trim(), parseError, parseGetParams, @@ -308,22 +306,10 @@ router.put('/org/:shortname', } } */ + param(['registry']).optional().isBoolean(), mw.validateUser, mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['new_short_name', 'id_quota', 'name', 'active_roles.add', 'active_roles.remove']) }), - query(['new_short_name', 'id_quota', 'name', 'active_roles.add', 'active_roles.remove']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - query(['new_short_name']).optional().isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - query(['id_quota']).optional().not().isArray().isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }).withMessage(errorMsgs.ID_QUOTA), - query(['name']).optional().isString().trim().notEmpty(), - query(['active_roles.add']).optional().toArray() - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole).withMessage(errorMsgs.ORG_ROLES), - query(['active_roles.remove']).optional().toArray() - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole).withMessage(errorMsgs.ORG_ROLES), + validateUpdateOrgParameters(), parseError, parsePostParams, controller.ORG_UPDATE_SINGLE) @@ -394,6 +380,7 @@ router.get('/org/:shortname/id_quota', } */ mw.validateUser, + param(['registry']).optional().isBoolean(), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), parseError, parseGetParams, @@ -466,6 +453,7 @@ router.get('/org/:shortname/users', } */ mw.validateUser, + param(['registry']).optional().isBoolean(), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), parseError, @@ -632,6 +620,7 @@ router.get('/org/:shortname/user/:username', } */ mw.validateUser, + param(['registry']).optional().isBoolean(), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), parseError, @@ -813,6 +802,7 @@ router.put('/org/:shortname/user/:username/reset_secret', */ mw.validateUser, mw.onlyOrgWithPartnerRole, + param(['registry']).optional().isBoolean(), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), parseError, diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 02cc4dc56..8a61db65a 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -1,12 +1,16 @@ require('dotenv').config() +const mongoose = require('mongoose') const User = require('../../model/user') const Org = require('../../model/org') +const RegistryOrg = require('../../model/registry-org') +const RegistryUser = require('../../model/registry-user') const logger = require('../../middleware/logger') const argon2 = require('argon2') const getConstants = require('../../constants').getConstants const cryptoRandomString = require('crypto-random-string') const uuid = require('uuid') const errors = require('./error') +const RegistryOrgRepository = require('../../repositories/registryOrgRepository') const error = new errors.OrgControllerError() const validateUUID = require('uuid').validate const booleanIsTrue = require('../../utils/utils').booleanIsTrue @@ -18,6 +22,7 @@ const booleanIsTrue = require('../../utils/utils').booleanIsTrue async function getOrgs (req, res, next) { try { const CONSTANTS = getConstants() + const isRegistry = req.query.registry === 'true' // temporary measure to allow tests to work after fixing #920 // tests required changing the global limit to force pagination @@ -28,9 +33,17 @@ async function getOrgs (req, res, next) { const options = CONSTANTS.PAGINATOR_OPTIONS options.sort = { short_name: 'asc' } options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value - const repo = req.ctx.repositories.getOrgRepository() - const agt = setAggregateOrgObj({}) + let agt + let repo + if (isRegistry) { + repo = req.ctx.repositories.getRegistryOrgRepository() + agt = setAggregateRegistryOrgObj({}) + } else { + repo = req.ctx.repositories.getOrgRepository() + agt = setAggregateOrgObj({}) + } + const pg = await repo.aggregatePaginate(agt, options) const payload = { organizations: pg.itemsList } @@ -56,18 +69,23 @@ async function getOrgs (req, res, next) { **/ async function getOrg (req, res, next) { try { + const isRegistry = req.query.registry === 'true' + const orgShortName = req.ctx.org const identifier = req.ctx.params.identifier - const repo = req.ctx.repositories.getOrgRepository() + + const repo = isRegistry ? req.ctx.repositories.getRegistryOrgRepository() : req.ctx.repositories.getOrgRepository() + const isSecretariat = await repo.isSecretariat(orgShortName) const org = await repo.findOneByShortName(orgShortName) let orgIdentifer = orgShortName - let agt = setAggregateOrgObj({ short_name: identifier }) + + let agt = isRegistry ? setAggregateRegistryOrgObj({ short_name: identifier }) : setAggregateOrgObj({ short_name: identifier }) // check if identifier is uuid and if so, reassign agt and orgIdentifier if (validateUUID(identifier)) { orgIdentifer = org.UUID - agt = setAggregateOrgObj({ UUID: identifier }) + agt = isRegistry ? setAggregateRegistryOrgObj({ UUID: identifier }) : setAggregateOrgObj({ UUID: identifier }) } if (orgIdentifer !== identifier && !isSecretariat) { @@ -97,6 +115,7 @@ async function getOrg (req, res, next) { async function getUsers (req, res, next) { try { const CONSTANTS = getConstants() + const isRegistry = req.query.registry === 'true' // temporary measure to allow tests to work after fixing #920 // tests required changing the global limit to force pagination @@ -109,8 +128,9 @@ async function getUsers (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 const shortName = req.ctx.org const orgShortName = req.ctx.params.shortname - const orgRepo = req.ctx.repositories.getOrgRepository() - const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = isRegistry ? req.ctx.repositories.getRegistryOrgRepository() : req.ctx.repositories.getOrgRepository() + const userRepo = isRegistry ? req.ctx.repositories.getRegistryUserRepository() : req.ctx.repositories.getUserRepository() + const orgUUID = await orgRepo.getOrgUUID(orgShortName) const isSecretariat = await orgRepo.isSecretariat(shortName) @@ -124,7 +144,7 @@ async function getUsers (req, res, next) { return res.status(403).json(error.notSameOrgOrSecretariat()) } - const agt = setAggregateUserObj({ org_UUID: orgUUID }) + const agt = isRegistry ? setAggregateRegistryUserObj({ 'cve_program_org_membership.program_org': orgUUID }) : setAggregateUserObj({ org_UUID: orgUUID }) const pg = await userRepo.aggregatePaginate(agt, options) const payload = { users: pg.itemsList } @@ -150,10 +170,12 @@ async function getUsers (req, res, next) { **/ async function getUser (req, res, next) { try { + const isRegistry = req.query.registry === 'true' const shortName = req.ctx.org const username = req.ctx.params.username const orgShortName = req.ctx.params.shortname - const orgRepo = req.ctx.repositories.getOrgRepository() + + const orgRepo = isRegistry ? req.ctx.repositories.getRegistryOrgRepository() : req.ctx.repositories.getOrgRepository() const isSecretariat = await orgRepo.isSecretariat(shortName) if (orgShortName !== shortName && !isSecretariat) { @@ -167,8 +189,8 @@ async function getUser (req, res, next) { return res.status(404).json(error.orgDnePathParam(orgShortName)) } - const userRepo = req.ctx.repositories.getUserRepository() - const agt = setAggregateUserObj({ username: username, org_UUID: orgUUID }) + const userRepo = isRegistry ? req.ctx.repositories.getRegistryUserRepository() : req.ctx.repositories.getUserRepository() + const agt = isRegistry ? setAggregateRegistryUserObj({ user_id: username, 'cve_program_org_membership.program_org': orgUUID }) : setAggregateUserObj({ username: username, org_UUID: orgUUID }) let result = await userRepo.aggregate(agt) result = result.length > 0 ? result[0] : null @@ -190,9 +212,11 @@ async function getUser (req, res, next) { **/ async function getOrgIdQuota (req, res, next) { try { + const isRegistry = req.query.registry === 'true' const orgShortName = req.ctx.org const shortName = req.ctx.params.shortname - const repo = req.ctx.repositories.getOrgRepository() + + const repo = isRegistry ? req.ctx.repositories.getRegistryOrgRepository() : req.ctx.repositories.getOrgRepository() const isSecretariat = await repo.isSecretariat(orgShortName) if (orgShortName !== shortName && !isSecretariat) { @@ -207,7 +231,7 @@ async function getOrgIdQuota (req, res, next) { } const returnPayload = { - id_quota: result.policies.id_quota, + ...(isRegistry ? { hard_quota: result.hard_quota } : { id_quota: result.policies.id_quota }), total_reserved: null, available: null } @@ -219,7 +243,11 @@ async function getOrgIdQuota (req, res, next) { const cveIdRepo = req.ctx.repositories.getCveIdRepository() result = await cveIdRepo.countDocuments(query) returnPayload.total_reserved = result - returnPayload.available = returnPayload.id_quota - returnPayload.total_reserved + if (isRegistry) { + returnPayload.available = returnPayload.hard_quota - returnPayload.total_reserved + } else { + returnPayload.available = returnPayload.id_quota - returnPayload.total_reserved + } logger.info({ uuid: req.ctx.uuid, message: 'The organization\'s id quota was returned to the user.', details: returnPayload }) return res.status(200).json(returnPayload) @@ -234,81 +262,190 @@ async function getOrgIdQuota (req, res, next) { * Called by POST /api/org/ **/ async function createOrg (req, res, next) { - const CONSTANTS = getConstants() + const isRegistry = req.query.registry === 'true' + + let payload = null + let responseMessage = null try { - const newOrg = new Org() + const legOrg = new Org() + const regOrg = new RegistryOrg() + const orgRepo = req.ctx.repositories.getOrgRepository() + const regOrgRepo = req.ctx.repositories.getRegistryOrgRepository() - for (const k in req.ctx.body) { - const key = k.toLowerCase() + const body = req.ctx.body + const keys = Object.keys(body) - switch (key) { - case 'short_name': - newOrg.short_name = req.ctx.body.short_name - break + // Short name is handled the same in leg and reg + const handlers = { + short_name: () => { + legOrg.short_name = body.short_name + regOrg.short_name = body.short_name + }, + authority: () => { + if ('active_roles' in req.ctx.body.authority) { + legOrg.authority.active_roles = req.ctx.body.authority.active_roles + regOrg.authority.active_roles = req.ctx.body.authority.active_roles + } + } + } - case 'name': - newOrg.name = req.ctx.body.name - break + if (isRegistry) { + // Reg only handlers + handlers.long_name = () => { + legOrg.name = body.long_name + regOrg.long_name = body.long_name + } - case 'authority': - if ('active_roles' in req.ctx.body.authority) { - newOrg.authority.active_roles = req.ctx.body.authority.active_roles - } - break + handlers.cve_program_org_function = () => { + regOrg.cve_program_org_function = body.cve_program_org_function + } - case 'policies': - if ('id_quota' in req.ctx.body.policies) { - newOrg.policies.id_quota = req.ctx.body.policies.id_quota - } - break + handlers.oversees = () => { + regOrg.oversees = body.oversees + } + handlers.hard_quota = () => { + regOrg.hard_quota = body.hard_quota + } + handlers.root_or_tlr = () => { + regOrg.root_or_tlr = body.root_or_tlr + } + handlers.charter_or_scope = () => { + regOrg.charter_or_scope = body.charter_or_scope + } + handlers.disclosure_policy = () => { + regOrg.disclosure_policy = body.disclosure_policy + } + handlers.product_list = () => { + regOrg.product_list = body.product_list + } + handlers.reports_to = () => { + regOrg.reports_to = body.reports_to + } - case 'uuid': - return res.status(400).json(error.uuidProvided('org')) + handlers['contact_info.poc'] = () => { + regOrg.contact_info.poc = body.contact_info.poc + } + handlers['contact_info.poc_email'] = () => { + regOrg.contact_info.poc_email = body.contact_info.poc_email + } + handlers['contact_info.poc_phone'] = () => { + regOrg.contact_info.poc_phone = body.contact_info.poc_phone + } + handlers['contact_info.org_email'] = () => { + regOrg.contact_info.org_email = body.contact_info.org_email + } + handlers['contact_info.website'] = () => { + regOrg.contact_info.website = body.contact_info.website + } + } else { + // Leg only handlers + handlers.name = () => { + legOrg.name = body.name + regOrg.long_name = body.name } - } - let result = await orgRepo.findOneByShortName(newOrg.short_name) // Find org in MongoDB - if (result) { - logger.info({ uuid: req.ctx.uuid, message: newOrg.short_name + ' organization was not created because it already exists.' }) - return res.status(400).json(error.orgExists(newOrg.short_name)) + handlers.policies = () => { + if ('id_quota' in req.ctx.body.policies) { + legOrg.policies.id_quota = req.ctx.body.policies.id_quota + regOrg.hard_quota = req.ctx.body.policies.id_quota + } + } } - newOrg.inUse = false - newOrg.UUID = uuid.v4() + for (const keyRaw of keys) { + const key = keyRaw.toLowerCase() + if (key === 'uuid') { + return res.status(400).json(error.uuidProvided('user')) + } - if (newOrg.authority.active_roles.length === 0) { // default is to make the Org a CNA if no role is specified - newOrg.authority.active_roles = [CONSTANTS.AUTH_ROLE_ENUM.CNA] + if (handlers[key]) { + handlers[key]() + } } + const session = await mongoose.startSession() + let legResult = null + let regResult = null + try { + session.startTransaction() + legResult = await orgRepo.findOneByShortName(legOrg.short_name, { session }) // Find org in MongoDB + regResult = await regOrgRepo.findOneByShortName(regOrg.short_name, { session }) // Find org in registry + + if (legResult && regResult) { + logger.info({ uuid: req.ctx.uuid, message: legResult.short_name + ' organization was not created because it already exists.' }) + return res.status(400).json(error.orgExists(legOrg.short_name)) + } - if (newOrg.policies.id_quota === undefined) { // set to default quota if none is specified - newOrg.policies.id_quota = CONSTANTS.DEFAULT_ID_QUOTA - } + legOrg.inUse = false + regOrg.inUse = false + const sharedUuid = uuid.v4() + legOrg.UUID = sharedUuid + regOrg.UUID = sharedUuid - if (newOrg.authority.active_roles.length === 1 && newOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 - newOrg.policies.id_quota = 0 - } + if (legOrg.authority.active_roles.length === 1 && legOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 + legOrg.policies.id_quota = 0 + } - await orgRepo.updateByOrgUUID(newOrg.UUID, newOrg, { upsert: true }) // Create org in MongoDB if it doesn't exist - const agt = setAggregateOrgObj({ short_name: newOrg.short_name }) - result = await orgRepo.aggregate(agt) - result = result.length > 0 ? result[0] : null + if (regOrg.authority.active_roles.length === 1 && regOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 + regOrg.hard_quota = 0 + } - const responseMessage = { - message: newOrg.short_name + ' organization was successfully created.', - created: result - } + await orgRepo.updateByOrgUUID(legOrg.UUID, legOrg, { session, upsert: true }) + await regOrgRepo.updateByUUID(regOrg.UUID, regOrg, { session, upsert: true }) - const payload = { - action: 'create_org', - change: newOrg.short_name + ' organization was successfully created.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - org: result + const legAgt = setAggregateOrgObj({ short_name: legOrg.short_name }) + const regAgt = setAggregateRegistryOrgObj({ short_name: regOrg.short_name }) + + legResult = await orgRepo.aggregate(legAgt, { session }) + legResult = legResult.length > 0 ? legResult[0] : null + + regResult = await regOrgRepo.aggregate(regAgt, { session }) + regResult = regResult.length > 0 ? regResult[0] : null + + if (isRegistry) { + responseMessage = { + message: regOrg.short_name + ' organization was successfully created.', + created: regResult + } + + payload = { + action: 'create_org', + change: regOrg.short_name + ' organization was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await regOrgRepo.getOrgUUID(req.ctx.org), + org: regResult + } + } else { + responseMessage = { + message: legOrg.short_name + ' organization was successfully created.', + created: legResult + } + + payload = { + action: 'create_org', + change: legOrg.short_name + ' organization was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org, { session }), + org: legResult + } + } + + const userRepo = req.ctx.repositories.getUserRepository() + const userRegistryRepo = req.ctx.repositories.getRegistryUserRepository() + if (isRegistry) { + payload.user_UUID = await userRegistryRepo.getUserUUID(req.ctx.user, payload.org_UUID, { session }) + } else { + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID, { session }) + } + await session.commitTransaction() + } catch (error) { + await session.abortTransaction() + throw error + } finally { + session.endSession() } - const userRepo = req.ctx.repositories.getUserRepository() - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) return res.status(200).json(responseMessage) } catch (err) { @@ -322,105 +459,238 @@ async function createOrg (req, res, next) { * Called by PUT /api/org/{shortname} **/ async function updateOrg (req, res, next) { + const isRegistry = req.query.registry === 'true' + let responseMessage = null + let payload = null + + const session = await mongoose.startSession() // Start a Mongoose session for transaction + try { - const shortName = req.ctx.params.shortname - const newOrg = new Org() - const removeRoles = [] - const addRoles = [] + session.startTransaction() + + const shortNameParam = req.ctx.params.shortname // The short_name from the URL path + const orgRepo = req.ctx.repositories.getOrgRepository() - const org = await orgRepo.findOneByShortName(shortName) - let agt = setAggregateOrgObj({ short_name: shortName }) + const regOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + const userRepo = req.ctx.repositories.getUserRepository() + const userRegistryRepo = req.ctx.repositories.getRegistryUserRepository() - // org doesn't exist - if (!org) { - logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated in MongoDB because it does not exist.' }) - return res.status(404).json(error.orgDnePathParam(shortName)) + // --- Unified Fetching Logic --- + const orgToUpdate = await orgRepo.findOneByShortName(shortNameParam, { session }) + + if (!orgToUpdate) { + logger.info({ uuid: req.ctx.uuid, message: `Organization ${shortNameParam} not found.` }) + await session.abortTransaction() + session.endSession() + return res.status(404).json(error.orgDnePathParam(shortNameParam)) } - Object.keys(req.ctx.query).forEach(k => { - const key = k.toLowerCase() + const regOrgToUpdate = await regOrgRepo.findOneByUUID(orgToUpdate.UUID, { session }) - if (key === 'new_short_name') { - newOrg.short_name = req.ctx.query.new_short_name - agt = setAggregateOrgObj({ short_name: newOrg.short_name }) - } else if (key === 'name') { - newOrg.name = req.ctx.query.name - } else if (key === 'id_quota') { - newOrg.policies.id_quota = req.ctx.query.id_quota - } else if (key === 'active_roles.add') { - if (Array.isArray(req.ctx.query['active_roles.add'])) { - req.ctx.query['active_roles.add'].forEach(r => { - addRoles.push(r) - }) - } - } else if (key === 'active_roles.remove') { - if (Array.isArray(req.ctx.query['active_roles.remove'])) { - req.ctx.query['active_roles.remove'].forEach(r => { - removeRoles.push(r) - }) - } + if (!regOrgToUpdate) { + // This indicates an inconsistent state, as an Org should have a corresponding RegistryOrg if created by the system + logger.error({ uuid: req.ctx.uuid, message: `Registry org counterpart for ${orgToUpdate.short_name} (UUID: ${orgToUpdate.UUID}) not found. Data inconsistency.` }) + await session.abortTransaction() + session.endSession() + return res.status(500).json(error.serverError('Inconsistent organization data: Registry counterpart missing.')) + } + + const newOrgUpdates = new Org() // For legacy org changes + const newRegOrgUpdates = new RegistryOrg() // For registry org changes + + const queryParams = req.ctx.query + const keys = Object.keys(queryParams) + // Initialize with the current short_name, will be updated if 'new_short_name' handler is called + let newShortNameForAggregation = orgToUpdate.short_name + + const addRolesCollector = [] + const removeRolesCollector = [] + + // Define handlers + const handlers = {} + + // --- Shared Handlers --- + handlers.new_short_name = () => { + const newShort = queryParams.new_short_name + if (newShort && typeof newShort === 'string' && newShort.trim() !== '') { // ensure newShort is valid + newOrgUpdates.short_name = newShort + newRegOrgUpdates.short_name = newShort + newShortNameForAggregation = newShort } - }) + } - // updating the org's roles - if (org) { - const roles = org.authority.active_roles + handlers['active_roles.add'] = () => { + const rolesFromQuery = queryParams['active_roles.add'] + if (rolesFromQuery) (Array.isArray(rolesFromQuery) ? rolesFromQuery : [rolesFromQuery]).forEach(r => addRolesCollector.push(r)) + } - // adding roles - addRoles.forEach(role => { - if (!roles.includes(role)) { - roles.push(role) + handlers['active_roles.remove'] = () => { + const rolesFromQuery = queryParams['active_roles.remove'] + if (rolesFromQuery) (Array.isArray(rolesFromQuery) ? rolesFromQuery : [rolesFromQuery]).forEach(r => removeRolesCollector.push(r)) + } + + // --- Conditional Handlers (controlled by isRegistry) --- + if (isRegistry) { + // Registry-focused updates + // In general, these do not have a direct effect on Legacy Orgs, so they are handled separately + handlers.long_name = () => { + const value = queryParams.long_name + if (value !== undefined) { + newOrgUpdates.name = value + newRegOrgUpdates.long_name = value + } + } + handlers.hard_quota = () => { + const value = queryParams.hard_quota + newOrgUpdates.policies.id_quota = value + newRegOrgUpdates.hard_quota = value + } + handlers.cve_program_org_function = () => { + if (queryParams.cve_program_org_function !== undefined) newRegOrgUpdates.cve_program_org_function = queryParams.cve_program_org_function + } + handlers.oversees = () => { + if (queryParams.oversees !== undefined) newRegOrgUpdates.oversees = queryParams.oversees + } + handlers.root_or_tlr = () => { + if (queryParams.root_or_tlr !== undefined) newRegOrgUpdates.root_or_tlr = queryParams.root_or_tlr + } + handlers.charter_or_scope = () => { + if (queryParams.charter_or_scope !== undefined) newRegOrgUpdates.charter_or_scope = queryParams.charter_or_scope + } + handlers.disclosure_policy = () => { + if (queryParams.disclosure_policy !== undefined) newRegOrgUpdates.disclosure_policy = queryParams.disclosure_policy + } + handlers.product_list = () => { + if (queryParams.product_list !== undefined) newRegOrgUpdates.product_list = queryParams.product_list + } + handlers.reports_to = () => { + if (queryParams.reports_to !== undefined) newRegOrgUpdates.reports_to = queryParams.reports_to + }; + // Contact Info for Registry Org + ['contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website'].forEach(field => { + handlers[field] = () => { + const fieldKeys = field.split('.') + if (queryParams[field] !== undefined) { + if (!newRegOrgUpdates[fieldKeys[0]]) newRegOrgUpdates[fieldKeys[0]] = {} + newRegOrgUpdates[fieldKeys[0]][fieldKeys[1]] = queryParams[field] + } } }) + } else { + // Legacy-focused updates (some sync to registry org) + handlers.name = () => { + const value = queryParams.name + if (value !== undefined) { + newOrgUpdates.name = value + newRegOrgUpdates.long_name = value + } + } + handlers.id_quota = () => { + const value = queryParams.id_quota + if (value !== undefined) { + if (!newOrgUpdates.policies) newOrgUpdates.policies = {} + newOrgUpdates.policies.id_quota = value + newRegOrgUpdates.hard_quota = value + } + } + } - // removing roles - removeRoles.forEach(role => { - const index = roles.indexOf(role) + for (const keyRaw of keys) { + const key = keyRaw.toLowerCase() + if (handlers[key]) { + handlers[key]() + } + } - if (index > -1) { - roles.splice(index, 1) - } + // Process collected role changes and sync them + if (addRolesCollector.length > 0 || removeRolesCollector.length > 0) { + const baseRoles = orgToUpdate.authority && orgToUpdate.authority.active_roles ? [...orgToUpdate.authority.active_roles] : [] + + addRolesCollector.forEach(role => { + if (!baseRoles.includes(role)) baseRoles.push(role) }) + const finalRoles = baseRoles.filter(role => !removeRolesCollector.includes(role)) - newOrg.authority.active_roles = roles + if (!newOrgUpdates.authority) newOrgUpdates.authority = {} + newOrgUpdates.authority.active_roles = finalRoles + if (!newRegOrgUpdates.authority) newRegOrgUpdates.authority = {} + newRegOrgUpdates.authority.active_roles = finalRoles // Sync roles } - if (newOrg.short_name) { - const result = await orgRepo.findOneByShortName(newOrg.short_name) + // ADP Quota override logic + if (newOrgUpdates.authority && newOrgUpdates.authority.active_roles) { // Check if roles were potentially modified + if (newOrgUpdates.authority.active_roles.length === 1 && newOrgUpdates.authority.active_roles[0] === 'ADP') { + if (!newOrgUpdates.policies) newOrgUpdates.policies = {} + newOrgUpdates.policies.id_quota = 0 + newRegOrgUpdates.hard_quota = 0 // Sync ADP quota + } + } - if (result) { - return res.status(403).json(error.duplicateShortname(newOrg.short_name)) + // Check for duplicate short_name if it's being changed + if (newOrgUpdates.short_name && newOrgUpdates.short_name !== orgToUpdate.short_name) { + const existingLegOrg = await orgRepo.findOneByShortName(newOrgUpdates.short_name, { session }) + if (existingLegOrg && existingLegOrg.UUID !== orgToUpdate.UUID) { + await session.abortTransaction(); session.endSession() + return res.status(403).json(error.duplicateShortname(newOrgUpdates.short_name)) + } + const existingRegOrg = await regOrgRepo.findOneByShortName(newRegOrgUpdates.short_name, { session }) + if (existingRegOrg && existingRegOrg.UUID !== regOrgToUpdate.UUID) { + await session.abortTransaction(); session.endSession() + return res.status(403).json(error.duplicateShortname(newRegOrgUpdates.short_name)) } } - // update org - let result = await orgRepo.updateByOrgUUID(org.UUID, newOrg) - if (result.matchedCount === 0) { - logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated in MongoDB because it does not exist.' }) - return res.status(404).json(error.orgDnePathParam(shortName)) + // Helper to check if an update object has actual data to set + const hasChanges = (updateObj) => { + if (!updateObj) return false + const topLevelKeys = Object.keys(updateObj).filter(k => typeof updateObj[k] !== 'object' || updateObj[k] === null) + if (topLevelKeys.length > 0) return true + if (updateObj.policies && Object.keys(updateObj.policies).length > 0) return true + if (updateObj.authority && Object.keys(updateObj.authority).length > 0) return true + if (updateObj.contact_info && Object.keys(updateObj.contact_info).length > 0) return true + // Add checks for other nested objects if any + return false } - result = await orgRepo.aggregate(agt) - result = result.length > 0 ? result[0] : null + if (hasChanges(newOrgUpdates)) { + console.log('DEBUG: Session ID object before update:', JSON.stringify(session.id)) + await orgRepo.updateByOrgUUID(orgToUpdate.UUID, newOrgUpdates, { session, upsert: false }) + } - const responseMessage = { - message: shortName + ' organization was successfully updated.', - updated: result + if (hasChanges(newRegOrgUpdates)) { + await regOrgRepo.updateByUUID(regOrgToUpdate.UUID, newRegOrgUpdates, { session, upsert: false }) } - const payload = { - action: 'update_org', - change: shortName + ' organization was successfully updated.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - org: result + let finalOrgState + // Response shaping controlled by isRegistry + if (isRegistry) { + const regAgt = setAggregateRegistryOrgObj({ short_name: newShortNameForAggregation }) + finalOrgState = (await regOrgRepo.aggregate(regAgt, { session }))[0] || null + responseMessage = { message: `${orgToUpdate.short_name} (Registry View) was successfully updated.`, updated: finalOrgState } // Clarify message + payload = { action: 'update_org', change: `${orgToUpdate.short_name} (Registry View) was successfully updated.`, org: finalOrgState } + payload.user_UUID = await userRegistryRepo.getUserUUID(req.ctx.user, regOrgToUpdate.UUID, { session }) + payload.org_UUID = regOrgToUpdate.UUID + } else { + const legAgt = setAggregateOrgObj({ short_name: newShortNameForAggregation }) + finalOrgState = (await orgRepo.aggregate(legAgt, { session }))[0] || null + responseMessage = { message: `${orgToUpdate.short_name} (Legacy View) was successfully updated.`, updated: finalOrgState } // Clarify message + payload = { action: 'update_org', change: `${orgToUpdate.short_name} (Legacy View) was successfully updated.`, org: finalOrgState } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, orgToUpdate.UUID, { session }) + payload.org_UUID = orgToUpdate.UUID } - const userRepo = req.ctx.repositories.getUserRepository() - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + + payload.req_UUID = req.ctx.uuid + + await session.commitTransaction() logger.info(JSON.stringify(payload)) return res.status(200).json(responseMessage) } catch (err) { + if (session.inTransaction()) { + await session.abortTransaction() + } next(err) + } finally { + session.endSession() } } @@ -756,47 +1026,84 @@ async function updateUser (req, res, next) { // Called by PUT /org/{shortname}/user/{username}/reset_secret async function resetSecret (req, res, next) { + const session = await mongoose.startSession() + session.startTransaction() try { + let randomKey + const requesterShortName = req.ctx.org const requesterUsername = req.ctx.user const username = req.ctx.params.username const orgShortName = req.ctx.params.shortname + const userRepo = req.ctx.repositories.getUserRepository() const orgRepo = req.ctx.repositories.getOrgRepository() - const isSecretariat = await orgRepo.isSecretariat(requesterShortName) - const orgUUID = await orgRepo.getOrgUUID(orgShortName) // userUUID may be null if user does not exist - if (!orgUUID) { - logger.info({ uuid: req.ctx.uuid, messsage: orgShortName + ' organization does not exist.' }) - return res.status(404).json(error.orgDnePathParam(orgShortName)) - } - if (orgShortName !== requesterShortName && !isSecretariat) { - logger.info({ uuid: req.ctx.uuid, message: orgShortName + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) - return res.status(403).json(error.notSameOrgOrSecretariat()) - } + const userRegistryRepo = req.ctx.repositories.getRegistryUserRepository() + const orgRegistryRepo = req.ctx.repositories.getRegistryOrgRepository() - const oldUser = await userRepo.findOneByUserNameAndOrgUUID(username, orgUUID) - if (!oldUser) { - logger.info({ uuid: req.ctx.uuid, messsage: username + ' user does not exist.' }) - return res.status(404).json(error.userDne(username)) - } + try { + const isSecretariatLeg = await orgRepo.isSecretariat(requesterShortName, { session }) + const isSecretariatReg = await orgRegistryRepo.isSecretariat(requesterShortName, { session }) + const isSecretariat = isSecretariatLeg && isSecretariatReg - const isAdmin = await userRepo.isAdmin(requesterUsername, requesterShortName) - // check if the user is not the requester or if the requester is not a secretariat - if ((orgShortName !== requesterShortName || username !== requesterUsername) && !isSecretariat) { - // check if the requester is not and admin; if admin, the requester must be from the same org as the user - if (!isAdmin || (isAdmin && orgShortName !== requesterShortName)) { - logger.info({ uuid: req.ctx.uuid, message: 'The api secret can only be reset by the Secretariat, an Org admin or if the requester is the user.' }) - return res.status(403).json(error.notSameUserOrSecretariat()) + const orgUUID = await orgRepo.getOrgUUID(orgShortName, { session }) // userUUID may be null if user does not exist + const orgRegUUID = await orgRegistryRepo.getOrgUUID(orgShortName, { session }) + + // check if orgUUID and orgRegUUID are the same + if (orgUUID.toString() !== orgRegUUID.toString()) { + logger.info({ uuid: req.ctx.uuid, message: 'The organization UUID and the organization registry UUID are not the same.' }) + return res.status(500).json(error.internalServerError()) } - } - const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }) - oldUser.secret = await argon2.hash(randomKey) // store in db - const user = await userRepo.updateByUserNameAndOrgUUID(oldUser.username, orgUUID, oldUser) - if (user.matchedCount === 0) { - logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + orgShortName + ' organization.' }) - return res.status(404).json(error.userDne(username)) + if (!orgUUID && !orgRegUUID) { + logger.info({ uuid: req.ctx.uuid, messsage: orgShortName + ' organization does not exist.' }) + return res.status(404).json(error.orgDnePathParam(orgShortName)) + } + + if (orgShortName !== requesterShortName && !isSecretariat) { + logger.info({ uuid: req.ctx.uuid, message: orgShortName + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) + return res.status(403).json(error.notSameOrgOrSecretariat()) + } + + const oldUser = await userRepo.findOneByUserNameAndOrgUUID(username, orgUUID, null, { session }) + const oldUserRegistry = await userRegistryRepo.findOneByUserNameAndOrgUUID(username, orgRegUUID) + + if (!oldUser && !oldUserRegistry) { + logger.info({ uuid: req.ctx.uuid, messsage: username + ' user does not exist.' }) + return res.status(404).json(error.userDne(username)) + } + + const isLegAdmin = await userRepo.isAdmin(requesterUsername, requesterShortName, { session }) + const isRegAdmin = await userRegistryRepo.isAdmin(requesterUsername, orgRegUUID, { session }) + const isAdmin = isLegAdmin && isRegAdmin + + // check if the user is not the requester or if the requester is not a secretariat + if ((orgShortName !== requesterShortName || username !== requesterUsername) && !isSecretariat) { + // check if the requester is not and admin; if admin, the requester must be from the same org as the user + if (!isAdmin || (isAdmin && orgShortName !== requesterShortName)) { + logger.info({ uuid: req.ctx.uuid, message: 'The api secret can only be reset by the Secretariat, an Org admin or if the requester is the user.' }) + return res.status(403).json(error.notSameUserOrSecretariat()) + } + } + + randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }) + oldUser.secret = await argon2.hash(randomKey) // store in db + oldUserRegistry.secret = await argon2.hash(randomKey) // store in db + + const user = await userRepo.updateByUserNameAndOrgUUID(oldUser.username, orgUUID, oldUser, { session }) + const userReg = await userRegistryRepo.updateByUserNameAndOrgUUID(oldUserRegistry.user_id, orgRegUUID, oldUserRegistry, { session }) + + if (user.matchedCount === 0 || userReg.matchedCount === 0) { + logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + orgShortName + ' organization.' }) + return res.status(404).json(error.userDne(username)) + } + await session.commitTransaction() + } catch (error) { + await session.abortTransaction() + throw error + } finally { + session.endSession() } logger.info({ uuid: req.ctx.uuid, message: `The API secret was successfully reset and sent to ${username}` }) @@ -815,6 +1122,7 @@ async function resetSecret (req, res, next) { } function setAggregateOrgObj (query) { + console.log('CRITICAL DEBUG: Query object received by setAggregateOrgObj:', JSON.stringify(query)) return [ { $match: query @@ -833,6 +1141,38 @@ function setAggregateOrgObj (query) { ] } +function setAggregateRegistryOrgObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + long_name: true, + short_name: true, + aliases: true, + cve_program_org_function: true, + authority: true, + reports_to: true, + oversees: true, + root_or_tlr: true, + users: true, + charter_or_scope: true, + disclosure_policy: true, + product_list: true, + soft_quota: true, + hard_quota: true, + contact_info: true, + in_use: true, + created: true, + last_updated: true + } + } + ] +} + function setAggregateUserObj (query) { return [ { @@ -852,7 +1192,28 @@ function setAggregateUserObj (query) { } ] } - +function setAggregateRegistryUserObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + user_id: true, + name: true, + org_affiliations: true, + cve_program_org_membership: true, + created: true, + created_by: true, + last_updated: true, + deactivation_date: true, + last_active: true + } + } + ] +} function parseUserName (newUser) { if (newUser.name) { if (!newUser.name.first) { diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 3913baeba..79341fd56 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -2,7 +2,13 @@ const getConstants = require('../../constants').getConstants const { validationResult } = require('express-validator') const errors = require('./error') const error = new errors.OrgControllerError() +const { body, param, query } = require('express-validator') +const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') +const CONSTANTS = getConstants() +const errorMsgs = require('../../middleware/errorMessages') const utils = require('../../utils/utils') +const mw = require('../../middleware/middleware') +const _ = require('lodash') function isOrgRole (val) { const CONSTANTS = getConstants() @@ -16,6 +22,207 @@ function isOrgRole (val) { return true } +function validateCreateOrgParameters () { + return async (req, res, next) => { + const useRegistry = req.query.registry === 'true' + let validations = [] + if (useRegistry) { + // Optional + // 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'] + validations = [ + body(['short_name']).isString() + .trim() + .notEmpty() + .isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['long_name']).isString() + .trim() + .notEmpty(), + body(['cve_program_org_function']) + .default('CNA') + .isString() + .isIn(orgOptions), + body(['oversees']).default([]) + .isArray(), + body(['root_or_tlr']).default(false) + .isBoolean(), + body( + [ + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'reports_to', + 'contact_info.poc', + 'contact_info.poc_email', + 'contact_info.poc_phone', + 'contact_info.org_email', + 'contact_info.website' + ]) + .default('') + .isString(), + body(['authority.active_roles']) + .default([CONSTANTS.AUTH_ROLE_ENUM.CNA]) + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole), + body(['hard_quota']) + .default(CONSTANTS.DEFAULT_ID_QUOTA) + .not() + .isArray() + .isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }) + .withMessage(errorMsgs.ID_QUOTA), + ...isNotAllowed('name', 'users', 'contact_info.admins', 'in_use', 'created', 'last_updated', 'policies.id_quota') + ] + } else { + validations = [ + body(['short_name']).isString() + .trim() + .notEmpty() + .isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['name']).isString() + .trim() + .notEmpty(), + body(['authority.active_roles']) + .default([CONSTANTS.AUTH_ROLE_ENUM.CNA]) + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole), + body(['policies.id_quota']) + .default(CONSTANTS.DEFAULT_ID_QUOTA) + .not() + .isArray() + .isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }) + .withMessage(errorMsgs.ID_QUOTA), + ...isNotAllowed( + 'oversees', + 'long_name', + 'cve_program_org_function', + 'contact_info.admins', + 'in_use', + 'created', + 'root_or_tlr', + 'soft_quota', + 'aliases', + 'hard_quota', + 'contact_info.org_email', + 'contact_info.website', + 'contact_info', + 'users', + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'reports_to', + 'contact_info.poc', + 'contact_info.poc_email', + 'contact_info.poc_phone', + 'contact_info.org_email', + 'contact_info.additional_contact_users', + 'contact_info.website') + ] + } + + for (const validation of validations) { + const result = await validation.run(req) + if (!result.isEmpty()) { + return res.status(400).json({ errors: result.array() }) + } + } + next() + } +} + +function validateUpdateOrgParameters () { + return async (req, res, next) => { + const useRegistry = req.query.registry === 'true' + + const legacyParametersOnly = ['id_quota', 'name'] + const registryParametersOnly = ['hard_quota', 'long_name', 'cve_program_org_function', 'oversees', 'root_or_tlr', 'charter_or_scope', 'disclosure_policy', 'product_list'] + const sharedParameters = ['new_short_name', 'active_roles.add', 'active_roles.remove', 'registry'] + + const allParameters = [ + ...legacyParametersOnly, ...registryParametersOnly, ...sharedParameters + ] + + const validations = [query().custom((query) => { return mw.validateQueryParameterNames(query, allParameters) }), + query(allParameters).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['new_short_name']).optional().isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query(['active_roles.add']).optional().toArray() + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole).withMessage(errorMsgs.ORG_ROLES), + query(['active_roles.remove']).optional().toArray() + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole).withMessage(errorMsgs.ORG_ROLES), + param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH })] + + if (useRegistry) { + validations.push( + + query(['hard_quota']).optional().not().isArray().isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }).withMessage(errorMsgs.ID_QUOTA), + query(['long_name']).optional().isString().trim().notEmpty(), + query(['oversees']).optional().isArray(), + query(['root_or_tlr']).optional().isBoolean(), + query( + [ + 'cve_program_org_function', + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'contact_info.poc', + 'contact_info.poc_email', + 'contact_info.poc_phone', + 'contact_info.org_email', + 'contact_info.website' + ]) + .optional() + .isString(), + ...isNotAllowedQuery(...legacyParametersOnly) + // if we decide that we want to allow more, we can add them here. + + ) + } else { + validations.push( + + query(['id_quota']).optional().not().isArray().isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }).withMessage(errorMsgs.ID_QUOTA), + query(['name']).optional().isString().trim().notEmpty(), + ...isNotAllowedQuery(...registryParametersOnly) + + ) + } + + for (const validation of validations) { + const result = await validation.run(req) + if (!result.isEmpty()) { + return res.status(400).json({ errors: result.array() }) + } + } + next() + } +} + +function isNotAllowed (...fields) { + return fields.map(field => + body(field) + .if((value, { req }) => _.has(req.body, field)) + .custom(() => { + throw new Error(`${field} must not be present`) + }) + ) +} + +function isNotAllowedQuery (...fields) { + return fields.map(field => + query(field) + .if((value, { req }) => _.has(req.query, field)) + .custom(() => { + throw new Error(`${field} must not be present`) + }) + ) +} + function isUserRole (val) { const CONSTANTS = getConstants() @@ -34,15 +241,24 @@ function parsePostParams (req, res, next) { 'new_short_name', 'name', 'id_quota', 'active', 'active_roles.add', 'active_roles.remove', 'new_username', 'org_short_name', - 'name.first', 'name.last', 'name.middle', 'name.suffix' + 'name.first', 'name.last', 'name.middle', 'name.suffix', 'long_name', 'cve_program_org_function', + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'contact_info.poc', + 'contact_info.poc_email', + 'contact_info.poc_phone', + 'contact_info.org_email', + 'hard_quota', + 'contact_info.website', 'root_or_tlr', 'oversees' ]) utils.reqCtxMapping(req, 'params', ['shortname', 'username']) next() } function parseGetParams (req, res, next) { - utils.reqCtxMapping(req, 'params', ['shortname', 'username', 'identifier']) - utils.reqCtxMapping(req, 'query', ['page']) + utils.reqCtxMapping(req, 'params', ['shortname', 'username', 'identifier', 'registry']) + utils.reqCtxMapping(req, 'query', ['page', 'registry']) next() } @@ -70,5 +286,7 @@ module.exports = { parseError, isOrgRole, isUserRole, - isValidUsername + isValidUsername, + validateCreateOrgParameters, + validateUpdateOrgParameters } diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry-org.controller/error.js new file mode 100644 index 000000000..dd5ffe352 --- /dev/null +++ b/src/controller/registry-org.controller/error.js @@ -0,0 +1,98 @@ +const idrErr = require('../../utils/error') + +class RegistryOrgControllerError extends idrErr.IDRError { + 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 + } + + 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 + } + + notAllowedToChangeOrganization () { + const err = {} + err.error = 'NOT_ALLOWED_TO_CHANGE_ORGANIZATION' + err.message = 'Only the Secretariat can change the organization for a user.' + return err + } + + orgExists (shortname) { // org + const err = {} + err.error = 'ORG_EXISTS' + err.message = `The '${shortname}' organization already exists.` + return err + } + + userExists (username) { // org + const err = {} + err.error = 'USER_EXISTS' + err.message = `The user '${username}' already exists.` + return err + } + + uuidProvided (creationType) { + const err = {} + err.error = 'UUID_PROVIDED' + err.message = `Providing UUIDs for ${creationType} creation or update is not allowed.` + return err + } + + duplicateUsername (shortname, username) { // org + const err = {} + err.error = 'DUPLICATE_USERNAME' + err.message = `The user could not be updated because the '${shortname}' organization contains another user with the username '${username}'.` + return err + } + + 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 + } + + duplicateShortname (shortname) { // org + const err = {} + err.error = 'DUPLICATE_SHORTNAME' + err.message = `The organization cannot be renamed as '${shortname}' because this shortname is used by another organization.` + return err + } + + paramDne (param) { // org + const err = {} + err.error = 'BAD_PARAMETER_NAME' + err.message = `'${param}' is not a valid parameter.` + return err + } + + notAllowedToSelfDemote () { + const err = {} + err.error = 'NOT_ALLOWED_TO_SELF_DEMOTE' + err.message = 'Please have another admin user from your organization change your role.' + return err + } + + userLimitReached () { + const err = {} + err.error = 'NUMBER_OF_USERS_IN_ORG_LIMIT_REACHED' + err.message = 'The requested user can not be created and added to the organization because the organization has hit its limit of 100 users. Contact the Secretariat.' + return err + } +} + +module.exports = { + RegistryOrgControllerError +} diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js new file mode 100644 index 000000000..a21348fa1 --- /dev/null +++ b/src/controller/registry-org.controller/index.js @@ -0,0 +1,547 @@ +const express = require('express') +const router = express.Router() +const mw = require('../../middleware/middleware') +const errorMsgs = require('../../middleware/errorMessages') +const { body, param, query } = require('express-validator') +const controller = require('./registry-org.controller') +const { parseGetParams, parsePostParams, parseDeleteParams, parseError, isOrgRole, isUserRole, isValidUsername } = require('./registry-org.middleware') +const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') +const getConstants = require('../../constants').getConstants +const CONSTANTS = getConstants() + +router.get('/registryOrg', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'getAllRegistryOrgs' + #swagger.summary = "Retrieves information about all registry organizations (accessible to Secretariat only)" + #swagger.description = " +Only users with Secretariat role can access this endpoint
+Secretariat: Retrieves a list of all registry organizations
+ #swagger.parameters['$ref'] = [ + '#/components/parameters/pageQuery', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'A list of all registry organizations, along with pagination fields if results span multiple pages of data', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-org/get-registry-org-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[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']) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), + // parseError, + parseGetParams, + controller.ALL_ORGS +) + +router.get('/registryOrg/:identifier', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'getSingleRegistryOrg' + #swagger.summary = "Retrieves information about a specific registry organization" + #swagger.description = " +All authenticated users can access this endpoint
+All Users: Retrieves information about the specified registry organization
+ #swagger.parameters['identifier'] = { + in: 'path', + description: 'The identifier of the registry organization', + required: true, + type: 'string' + } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'The requested registry organization information is returned', + content: { + "application/json": { + schema: { $ref: '../schemas/org/get-org-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[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, + param(['identifier']).isString().trim(), + // parseError, + parseGetParams, + controller.SINGLE_ORG +) + +router.post('/registryOrg', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'createRegistryOrg' + #swagger.summary = "Creates a new registry organization (accessible to Secretariat only)" + #swagger.description = " +Only users with Secretariat role can access this endpoint
+Secretariat: Creates a new registry organization
+ #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/CreateOrgPayload' } + } + } + } + #swagger.responses[201] = { + description: 'The registry organization was successfully created', + content: { + "application/json": { + schema: { $ref: '../schemas/org/create-org-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[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['long_name']).isString().trim().notEmpty(), + body(['authority.active_roles']).optional() + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole), + body(['soft_quota']).optional().not().isArray().isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }).withMessage(errorMsgs.ID_QUOTA), + body(['hard_quota']).optional().not().isArray().isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }).withMessage(errorMsgs.ID_QUOTA), + // TODO: more validation needed? + // parseError, + parsePostParams, + controller.CREATE_ORG +) + +router.put('/registryOrg/:shortname', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'updateRegistryOrg' + #swagger.summary = "Updates an existing registry organization (accessible to Secretariat only)" + #swagger.description = " +Only users with Secretariat role can access this endpoint
+Secretariat: Updates an existing registry organization
+ #swagger.parameters['shortname'] = { + in: 'path', + description: 'The Shortname of the registry organization to update', + required: true, + type: 'string' + } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/UpdateOrgPayload' } + } + } + } + #swagger.responses[200] = { + description: 'The registry organization was successfully updated', + content: { + "application/json": { + schema: { $ref: '../schemas/org/update-org-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, + param(['shortname']).isString().trim(), + // TODO: do more validation here + // parseError, + parsePostParams, + parseGetParams, + controller.UPDATE_ORG +) + +router.delete('/registryOrg/:identifier', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'deleteRegistryOrg' + #swagger.summary = "Deletes an existing registry organization (accessible to Secretariat only)" + #swagger.description = " +Only users with Secretariat role can access this endpoint
+Secretariat: Deletes an existing registry organization
+ #swagger.parameters['identifier'] = { + in: 'path', + description: 'The identifier of the registry organization to delete', + required: true, + type: 'string' + } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'The registry organization was successfully deleted', + content: { + "application/json": { + schema: { $ref: '../schemas/org/delete-org-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' } + } + } + } + */ + mw.validateUser, + // TODO: permissions + param(['identifier']).isString().trim(), + // parseError, + parseDeleteParams, + controller.DELETE_ORG +) + +console.log(controller.USER_ALL) + +router.get('/registryOrg/:shortname/users', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserOrgAll' + #swagger.summary = "Retrieves all users for the organization with the specified short name (accessible to all registered users)" + #swagger.description = " +All registered users can access this endpoint
+Regular, CNA & Admin Users: Retrieves information about users in the same organization
+Secretariat: Retrieves all user information for any organization
" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/pageQuery', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns all users for the organization, along with pagination fields if results span multiple pages of data', + content: { + "application/json": { + schema: { $ref: '../schemas/user/list-users-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, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), + parseError, + parseGetParams, + controller.USER_ALL) + +router.post('/registryOrg/:shortname/user', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'RegistryUserCreateSingle' + #swagger.summary = "Create a user with the provided short name as the owning organization (accessible to Admins and Secretariats)" + #swagger.description = " +User must belong to an organization with the Secretariat role or be an Admin of the organization
+Admin User: Creates a user for the Admin's organization
+Secretariat: Creates a user for any organization
" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { $ref: '../schemas/user/create-user-request.json' }, + } + } + } + #swagger.responses[200] = { + description: 'Returns the new user information (with the secret)', + content: { + "application/json": { + schema: { $ref: '../schemas/user/create-user-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.onlySecretariatOrAdmin(true), + // // mw.onlyOrgWithPartnerRole, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['cve_program_org_membership']) + .optional() + .custom(mw.isCveProgramOrgMembershipObject), + parseError, + parsePostParams, + controller.USER_CREATE_SINGLE) + +module.exports = router diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js new file mode 100644 index 000000000..5bbf18cfe --- /dev/null +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -0,0 +1,445 @@ +const uuid = require('uuid') +const logger = require('../../middleware/logger') +const { getConstants } = require('../../constants') +const RegistryOrg = require('../../model/registry-org') +const RegistryUser = require('../../model/registry-user') +const errors = require('./error') +const error = new errors.RegistryOrgControllerError() +const validateUUID = require('uuid').validate + +async function getAllOrgs (req, res, next) { + try { + const CONSTANTS = getConstants() + + // temporary measure to allow tests to work after fixing #920 + // tests required changing the global limit to force pagination + if (req.TEST_PAGINATOR_LIMIT) { + CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT + } + + const options = CONSTANTS.PAGINATOR_OPTIONS + options.sort = { short_name: 'asc' } + options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value + const repo = req.ctx.repositories.getRegistryOrgRepository() + const agt = setAggregateOrgObj({}) + const pg = await repo.aggregatePaginate(agt, options) + + await RegistryOrg.populateOverseesAndReportsTo(pg.itemsList) + await RegistryUser.populateUsers(pg.itemsList) + await RegistryUser.populateAdditionalContactUsers(pg.itemsList) + await RegistryUser.populateAdmins(pg.itemsList) + // Update UUIDS to objects + + const payload = { orgs: pg.itemsList } + + if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { + payload.totalCount = pg.itemCount + payload.itemsPerPage = pg.itemsPerPage + payload.pageCount = pg.pageCount + payload.currentPage = pg.currentPage + payload.prevPage = pg.prevPage + payload.nextPage = pg.nextPage + } + + logger.info({ uuid: req.ctx.uuid, message: 'The org information was sent to the secretariat user.' }) + return res.status(200).json(payload) + } catch (err) { + next(err) + } +} + +async function getOrg (req, res, next) { + try { + const repo = req.ctx.repositories.getRegistryOrgRepository() + const identifier = req.ctx.params.identifier + const orgShortName = req.ctx.org + const isSecretariat = await repo.isSecretariat(orgShortName) + const org = await repo.findOneByShortName(orgShortName) + let orgIdentifer = orgShortName + let agt = setAggregateOrgObj({ UUID: identifier }) + + if (validateUUID(identifier)) { + orgIdentifer = org.UUID + agt = setAggregateOrgObj({ UUID: identifier }) + } + + if (orgIdentifer !== 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()) + } + + let result = await repo.aggregate(agt) + result = result.length > 0 ? result[0] : null + // TODO: We need real error messages here pls and thanks + + if (!result) { + logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization does not exist.' }) + return res.status(404).json(error.orgDne(identifier, 'identifier', 'path')) + } + + logger.info({ uuid: req.ctx.uuid, message: identifier + ' org was sent to the user.', org: result }) + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +async function createOrg (req, res, next) { + try { + const CONSTANTS = getConstants() + const userRepo = req.ctx.repositories.getRegistryUserRepository() + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + const body = req.ctx.body + + const newOrg = new RegistryOrg() + Object.keys(body).map(k => k.toLowerCase()).forEach(k => { + if (k === 'long_name') { + newOrg.long_name = body[k] + } else if (k === 'short_name') { + newOrg.short_name = body[k] + } else if (k === 'aliases') { + newOrg.aliases = [...new Set(body[k].active_roles)] + } else if (k === 'cve_program_org_function') { + newOrg.cve_program_org_function = body[k] + } else if (k === 'authority') { + if ('active_roles' in body[k]) { + newOrg.authority.active_roles = [...new Set(body[k].active_roles)] + } + } else if (k === 'reports_to') { + // TODO: org check logic? + } else if (k === 'oversees') { + // TODO: org check logic? + } else if (k === 'root_or_tlr') { + newOrg.root_or_tlr = body[k] + } else if (k === 'users') { + // TODO: users logic? + } else if (k === 'charter_or_scope') { + newOrg.charter_or_scope = body[k] + } else if (k === 'disclosure_policy') { + newOrg.disclosure_policy = body[k] + } else if (k === 'product_list') { + newOrg.product_list = body[k] + } else if (k === 'soft_quota') { + newOrg.soft_quota = body[k] + } else if (k === 'hard_quota') { + newOrg.hard_quota = body[k] + } else if (k === 'contact_info') { + const { additional_contact_users, admins, ...contactInfo } = body[k] + newOrg.contact_info = { + additional_contact_users: [...(additional_contact_users || [])], + poc: '', + poc_email: '', + poc_phone: '', + admins: [...(admins || [])], + org_email: '', + website: '', + ...contactInfo + } + } else if (k === 'uuid') { + return res.status(400).json(error.uuidProvided('org')) + } + }) + + const doesExist = await registryOrgRepo.findOneByShortName(newOrg.short_name) + if (doesExist) { + logger.info({ uuid: req.ctx.uuid, message: newOrg.short_name + ' organization was not created because it already exists.' }) + return res.status(400).json(error.orgExists(newOrg.short_name)) + } + + if (newOrg.reports_to === undefined) { + // TODO: This may need to be set to mitre, will ask the awg + newOrg.reports_to = null + } + if (newOrg.root_or_tlr === undefined) { + newOrg.root_or_tlr = false + } + if (newOrg.soft_quota === undefined) { // set to default quota if none is specified + newOrg.soft_quota = CONSTANTS.DEFAULT_ID_QUOTA + } + if (newOrg.hard_quota === undefined) { // set to default quota if none is specified + newOrg.hard_quota = CONSTANTS.DEFAULT_ID_QUOTA + } + if (newOrg.authority.active_roles.length === 1 && newOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 + newOrg.soft_quota = 0 + newOrg.hard_quota = 0 + } + + newOrg.in_use = false + newOrg.UUID = uuid.v4() + + await registryOrgRepo.updateByUUID(newOrg.UUID, newOrg, { upsert: true }) + const agt = setAggregateOrgObj({ UUID: newOrg.UUID }) + let result = await registryOrgRepo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + const payload = { + action: 'create_registry_org', + change: result.short_name + ' was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await registryOrgRepo.getOrgUUID(req.ctx.org), + org: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + const responseMessage = { + message: result.short_name + ' was successfully created.', + created: result + } + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + +async function updateOrg (req, res, next) { + try { + const shortName = req.ctx.params.shortname + const userRepo = req.ctx.repositories.getRegistryUserRepository() + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + // const shortName = req.ctx.params.shortname + + const org = await registryOrgRepo.findOneByShortName(shortName) + if (!org) { + logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated in MongoDB because it does not exist.' }) + return res.status(404).json(error.orgDnePathParam(shortName)) + } + + const orgUUID = await registryOrgRepo.getOrgUUID(shortName) + + const newOrg = new RegistryOrg() + newOrg.contact_info = { ...org.contact_info } + + for (const k in req.ctx.query) { + const key = k.toLowerCase() + + if (key === 'long_name') { + newOrg.long_name = req.ctx.query.long_name + } else if (key === 'short_name') { + newOrg.short_name = req.ctx.query.short_name + } else if (key === 'aliases') { + // TODO: handle aliases + } else if (key === 'cve_program_org_function') { + newOrg.cve_program_org_function = req.ctx.query.cve_program_org_function + // TODO: validate against enum? + } else if (key === 'authority') { + // TODO: handle active_roles + } else if (key === 'reports_to') { + // TODO: validate org + } else if (key === 'oversees') { + // TODO: validate orgs + } else if (key === 'root_or_tlr') { + newOrg.root_or_tlr = req.ctx.query.root_or_tlr + } else if (key === 'users') { + // TODO: validate users + } else if (key === 'charter_or_scope') { + newOrg.charter_or_scope = req.ctx.query.charter_or_scope + } else if (key === 'disclosure_policy') { + newOrg.disclosure_policy = req.ctx.query.disclosure_policy + } else if (key === 'product_list') { + newOrg.product_list = req.ctx.query.product_list + } else if (key === 'soft_quota') { + newOrg.soft_quota = req.ctx.query.soft_quota + } else if (key === 'hard_quota') { + newOrg.hard_quota = req.ctx.query.hard_quota + } else if (key === 'contact_info.additional_contact_users') { + // TODO: validate users + } else if (key === 'contact_info.poc') { + newOrg.contact_info.poc = req.ctx.query['contact_info.poc'] + } else if (key === 'contact_info.poc_email') { + newOrg.contact_info.poc_email = req.ctx.query['contact_info.poc_email'] + } else if (key === 'contact_info.poc_phone') { + newOrg.contact_info.poc_phone = req.ctx.query['contact_info.poc_phone'] + } else if (key === 'contact_info.admins') { + // TODO: validate admins + } else if (key === 'contact_info.org_email') { + newOrg.contact_info.org_email = req.ctx.query['contact_info.org_email'] + } else if (key === 'contact_info.website') { + newOrg.contact_info.website = req.ctx.query['contact_info.website'] + } + } + + await registryOrgRepo.updateByUUID(orgUUID, newOrg) + const agt = setAggregateOrgObj({ UUID: orgUUID }) + let result = await registryOrgRepo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + const payload = { + action: 'update_registry_org', + change: result.short_name + ' was successfully updated.', + req_UUID: req.ctx.uuid, + org_UUID: await registryOrgRepo.getOrgUUID(req.ctx.org), + user: result + } + 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.short_name + ' was successfully updated.' + } else { + msgStr = 'No updates were specified for ' + result.short_name + '.' + } + const responseMessage = { + message: msgStr, + updated: result + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + +async function deleteOrg (req, res, next) { + try { + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + const orgUUID = req.ctx.params.identifier + + const org = await registryOrgRepo.findOneByUUID(orgUUID) + + await registryOrgRepo.deleteByUUID(orgUUID) + + const payload = { + action: 'delete_registry_org', + change: org.short_name + ' was successfully deleted.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org) + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + const responseMessage = { + message: org.short_name + ' was successfully deleted.' + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + +/** + * Get the details of all users from an org given the specified shortname + * Called by GET /api/org/{shortname}/users + **/ +async function getUsers (req, res, next) { + try { + const CONSTANTS = getConstants() + + // temporary measure to allow tests to work after fixing #920 + // tests required changing the global limit to force pagination + if (req.TEST_PAGINATOR_LIMIT) { + CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT + } + + const options = CONSTANTS.PAGINATOR_OPTIONS + options.sort = { username: 'asc' } + options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value + const shortName = req.ctx.org + const orgShortName = req.ctx.params.shortname + const orgRepo = req.ctx.repositories.getRegistryOrgRepository() + const userRepo = req.ctx.repositories.getRegistryUserRepository() + const orgUUID = await orgRepo.getOrgUUID(orgShortName) + const isSecretariat = await orgRepo.isSecretariat(shortName) + + if (!orgUUID) { + logger.info({ uuid: req.ctx.uuid, message: orgShortName + ' organization does not exist.' }) + return res.status(404).json(error.orgDnePathParam(orgShortName)) + } + + if (orgShortName !== shortName && !isSecretariat) { + logger.info({ uuid: req.ctx.uuid, message: orgShortName + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) + return res.status(403).json(error.notSameOrgOrSecretariat()) + } + + const agt = setAggregateUserObj({ 'org_affiliations.org_id': orgUUID }) + const pg = await userRepo.aggregatePaginate(agt, options) + const payload = { users: pg.itemsList } + + if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { + payload.totalCount = pg.itemCount + payload.itemsPerPage = pg.itemsPerPage + payload.pageCount = pg.pageCount + payload.currentPage = pg.currentPage + payload.prevPage = pg.prevPage + payload.nextPage = pg.nextPage + } + + logger.info({ uuid: req.ctx.uuid, message: `The users of ${orgShortName} organization were sent to the user.` }) + return res.status(200).json(payload) + } catch (err) { + next(err) + } +} + +function createUserByOrg (req, res, next) { + console.log('HERE') +} + +function setAggregateUserObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + user_id: true, + name: true, + org_affiliations: true, + cve_program_org_membership: true, + created: true, + created_by: true, + last_updated: true, + deactivation_date: true, + last_active: true + } + } + ] +} + +function setAggregateOrgObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + long_name: true, + short_name: true, + aliases: true, + cve_program_org_function: true, + authority: true, + reports_to: true, + oversees: true, + root_or_tlr: true, + users: true, + charter_or_scope: true, + disclosure_policy: true, + product_list: true, + soft_quota: true, + hard_quota: true, + contact_info: true, + in_use: true, + created: true, + last_updated: true + } + } + ] +} + +module.exports = { + ALL_ORGS: getAllOrgs, + SINGLE_ORG: getOrg, + CREATE_ORG: createOrg, + UPDATE_ORG: updateOrg, + DELETE_ORG: deleteOrg, + USER_ALL: getUsers, + USER_CREATE_SINGLE: createUserByOrg +} diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js new file mode 100644 index 000000000..f266edb32 --- /dev/null +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -0,0 +1,66 @@ +const utils = require('../../utils/utils') +const getConstants = require('../../constants').getConstants +const { validationResult } = require('express-validator') +const errors = require('./error') +const error = new errors.RegistryOrgControllerError() + +function parsePostParams (req, res, next) { + utils.reqCtxMapping(req, 'body', []) + utils.reqCtxMapping(req, 'params', ['identifier, shortname']) + utils.reqCtxMapping(req, 'query', [ + 'long_name', 'short_name', 'aliases', + 'cve_program_org_function', 'authority.active_roles', + 'reports_to', 'oversees', + 'root_or_tlr', 'users', + 'charter_or_scope', 'disclosure_policy', 'product_list', + 'soft_quota', 'hard_quota', + 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', + 'contact_info.admins', 'contact_info.org_email', 'contact_info.website' + ]) + next() +} + +function parseGetParams (req, res, next) { + utils.reqCtxMapping(req, 'params', ['identifier', 'shortname']) + utils.reqCtxMapping(req, 'query', ['page']) + next() +} + +function parseDeleteParams (req, res, next) { + utils.reqCtxMapping(req, 'params', ['identifier']) + next() +} + +function isUserRole (val) { + const constants = getConstants() +} + +function isOrgRole (val) { + const CONSTANTS = getConstants() + + val.forEach(role => { + if (!CONSTANTS.ORG_ROLES.includes(role)) { + throw new Error('Organization role does not exist.') + } + }) + + return true +} + +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(error.badInput(err.array())) + } + next() +} + +module.exports = { + parsePostParams, + parseGetParams, + parseError, + parseDeleteParams, + isOrgRole +} diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js new file mode 100644 index 000000000..756825b46 --- /dev/null +++ b/src/controller/registry-user.controller/index.js @@ -0,0 +1,380 @@ +const express = require('express') +const router = express.Router() +const mw = require('../../middleware/middleware') +const { body, param, query } = require('express-validator') +const controller = require('./registry-user.controller') +const { parseGetParams, parsePostParams, parseDeleteParams, parseError } = require('./registry-user.middleware') +const getConstants = require('../../constants').getConstants +const CONSTANTS = getConstants() + +router.get('/registryUser', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'getAllRegistryUsers' + #swagger.summary = "Retrieves information about all registry users (accessible to Secretariat only)" + #swagger.description = " +Only users with Secretariat role can access this endpoint
+Secretariat: Retrieves a list of all registry users
+ #swagger.parameters['$ref'] = [ + '#/components/parameters/pageQuery', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'A list of all registry organizations, along with pagination fields if results span multiple pages of data', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-user/get-registry-users-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[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + // parseError, + parseGetParams, + controller.ALL_USERS +) + +router.get('/registryUser/:identifier', +/* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'getSingleRegistryUser' + #swagger.summary = "Retrieves information about a specific registry user" + #swagger.description = " +All authenticated users can access this endpoint
+All Users: Retrieves information about the specified registry user
+ #swagger.parameters['identifier'] = { + in: 'path', + description: 'The identifier of the registry user', + required: true, + type: 'string' + } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'The requested registry user information is returned', + content: { + "application/json": { + schema: { $ref: '../schemas/user/get-user-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[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, + param(['identifier']).isString().trim(), + // parseError, + parseGetParams, + controller.SINGLE_USER +) + +router.post('/registryUser', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'createRegistryUser' + #swagger.summary = "Creates a new registry user (accessible to Secretariat only)" + #swagger.description = " +Only users with Secretariat role can access this endpoint
+Secretariat: Creates a new registry user
+ #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/CreateUserPayload' } + } + } + } + #swagger.responses[201] = { + description: 'The registry user was successfully created', + content: { + "application/json": { + schema: { $ref: '../schemas/user/create-user-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[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + // mw.onlySecretariat, // TODO: permissions + // TODO: validation + // parseError, + parsePostParams, + controller.CREATE_USER +) + +router.put('/registryUser/:identifier', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'updateRegistryUser' + #swagger.summary = "Updates an existing registry user (accessible to Secretariat only)" + #swagger.description = " +Only users with Secretariat role can access this endpoint
+Secretariat: Updates an existing registry user
+ #swagger.parameters['identifier'] = { + in: 'path', + description: 'The identifier of the registry user to update', + required: true, + type: 'string' + } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/UpdateUserPayload' } + } + } + } + #swagger.responses[200] = { + description: 'The registry user was successfully updated', + content: { + "application/json": { + schema: { $ref: '../schemas/user/update-user-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, + param(['identifier']).isString().trim(), + // TODO: do more validation here + // parseError, + parsePostParams, + controller.UPDATE_USER +) + +router.delete('/registryUser/:identifier', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'deleteRegistryUser' + #swagger.summary = "Deletes an existing registry user (accessible to Secretariat only)" + #swagger.description = " +Only users with Secretariat role can access this endpoint
+Secretariat: Deletes an existing registry user
+ #swagger.parameters['identifier'] = { + in: 'path', + description: 'The identifier of the registry user to delete', + required: true, + type: 'string' + } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'The registry user was successfully deleted', + content: { + "application/json": { + schema: { $ref: '../schemas/user/delete-user-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, + param(['identifier']).isString().trim(), + // parseError, + parseDeleteParams, + controller.DELETE_USER +) + +module.exports = router diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js new file mode 100644 index 000000000..d94072def --- /dev/null +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -0,0 +1,283 @@ +const argon2 = require('argon2') +const cryptoRandomString = require('crypto-random-string') +const uuid = require('uuid') +const logger = require('../../middleware/logger') +const { getConstants } = require('../../constants') +const RegistryUser = require('../../model/registry-user') +const RegistryOrg = require('../../model/registry-org') + +async function getAllUsers (req, res, next) { + try { + const CONSTANTS = getConstants() + + // temporary measure to allow tests to work after fixing #920 + // tests required changing the global limit to force pagination + if (req.TEST_PAGINATOR_LIMIT) { + CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT + } + + const options = CONSTANTS.PAGINATOR_OPTIONS + options.sort = { short_name: 'asc' } + options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value + const repo = req.ctx.repositories.getRegistryUserRepository() + + const agt = setAggregateUserObj({}) + const pg = await repo.aggregatePaginate(agt, options) + + await RegistryOrg.populateOrgAffiliations(pg.itemsList); + await RegistryOrg.populateCVEProgramOrgMembership(pg.itemsList); + + const payload = { users: pg.itemsList } + + if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { + payload.totalCount = pg.itemCount + payload.itemsPerPage = pg.itemsPerPage + payload.pageCount = pg.pageCount + payload.currentPage = pg.currentPage + payload.prevPage = pg.prevPage + payload.nextPage = pg.nextPage + } + + logger.info({ uuid: req.ctx.uuid, message: 'The user information was sent to the secretariat user.' }) + return res.status(200).json(payload) + } catch (err) { + next(err) + } +} + +async function getUser (req, res, next) { + try { + const repo = req.ctx.repositories.getRegistryUserRepository() + const identifier = req.ctx.params.identifier + const agt = setAggregateUserObj({ UUID: identifier }) + let result = await repo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + logger.info({ uuid: req.ctx.uuid, message: identifier + ' user was sent to the user.', user: result }) + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +async function createUser (req, res, next) { + try { + // const requesterUsername = req.ctx.user + // const requesterShortName = req.ctx.org + const orgRepo = req.ctx.repositories.getOrgRepository() + const userRepo = req.ctx.repositories.getUserRepository() + const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() + const body = req.ctx.body + + // TODO: check if affiliated orgs and program orgs exist, and if their membership limit is reached + + const newUser = new RegistryUser() + Object.keys(body).map(k => k.toLowerCase()).forEach(k => { + if (k === 'user_id' || k === 'username') { + newUser.user_id = body[k] + } else if (k === 'name') { + newUser.name = { + first: '', + last: '', + middle: '', + suffix: '', + ...body.name + } + } else if (k === 'org_affiliations') { + // TODO: dedupe + } else if (k === 'cve_program_org_membership') { + // TODO: dedupe + } else if (k === 'uuid') { + return res.status(400).json(error.uuidProvided('user')) + } + }) + + // TODO: check that requesting user is admin of org for new user + + newUser.UUID = uuid.v4() + const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }) + newUser.secret = await argon2.hash(randomKey) + newUser.last_active = null + newUser.deactivation_date = null + + await registryUserRepo.updateByUUID(newUser.UUID, newUser, { upsert: true }) + const agt = setAggregateUserObj({ UUID: newUser.UUID }) + let result = await registryUserRepo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + const payload = { + action: 'create_registry_user', + change: result.user_id + ' was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + user: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + result.secret = randomKey + const responseMessage = { + message: result.user_id + ' was successfully created.', + created: result + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + +async function updateUser (req, res, next) { + try { + const requesterShortName = req.ctx.org + const requesterUsername = req.ctx.user + // const username = req.ctx.params.username + // const shortName = req.ctx.params.shortname + const userUUID = req.ctx.params.identifier + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() + // const orgUUID = await orgRepo.getOrgUUID(shortName) + const isSecretariat = await orgRepo.isSecretariat(requesterShortName) + const isAdmin = await userRepo.isAdmin(requesterUsername, requesterShortName) // Check if requester is Admin of the designated user's org + + const user = await registryUserRepo.findOneByUUID(userUUID) + const newUser = new RegistryUser() + + // Sets the name values to what currently exists in the database, this ensures data is retained during partial name updates + newUser.name.first = user.name.first + newUser.name.last = user.name.last + newUser.name.middle = user.name.middle + newUser.name.suffix = user.name.suffix + + const queryParameterPermissions = { + new_user_id: true, + 'name.first': false, + 'name.last': false, + 'name.middle': false, + 'name.suffix': false, + 'org_affiliations.add': false, + 'org_affiliations.remove': false, + 'cve_program_org_membership.add': false, + 'cve_program_org_membership.remove': false + } + + // TODO: check permissions + // Check to ensure that the user has the right permissions to edit the fields tha they are requesting to edit, and fail fast if they do not. + // if (Object.keys(req.ctx.query).length > 0 && Object.keys(req.ctx.query).some((key) => { return queryParameterPermissions[key] }) && !(isAdmin || isSecretariat)) { + // logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + requesterUsername + ' user is not Org Admin or Secretariat to modify these fields.' }) + // return res.status(403).json(error.notOrgAdminOrSecretariatUpdate()) + // } + + for (const k in req.ctx.query) { + const key = k.toLowerCase() + + if (key === 'new_user_id') { + newUser.user_id = req.ctx.query.new_user_id + } else if (key === 'name.first') { + newUser.name.first = req.ctx.query['name.first'] + } else if (key === 'name.last') { + newUser.name.last = req.ctx.query['name.last'] + } else if (key === 'name.middle') { + newUser.name.middle = req.ctx.query['name.middle'] + } else if (key === 'name.suffix') { + newUser.name.suffix = req.ctx.query['name.suffix'] + } + + // TODO: process org affiliations and program org membership updates + } + + await registryUserRepo.updateByUUID(userUUID, newUser) + const agt = setAggregateUserObj({ UUID: userUUID }) + let result = await registryUserRepo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + const payload = { + action: 'update_registry_user', + change: result.user_id + ' was successfully updated.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + user: result + } + 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) + } catch (err) { + next(err) + } +} + +async function deleteUser (req, res, next) { + try { + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() + const userUUID = req.ctx.params.identifier + + const user = await registryUserRepo.findOneByUUID(userUUID) + + // TODO: check permissions + + await registryUserRepo.deleteByUUID(userUUID) + + const payload = { + action: 'delete_registry_user', + change: user.user_id + ' was successfully deleted.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org) + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + const responseMessage = { + message: user.user_id + ' was successfully deleted.' + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + +function setAggregateUserObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + user_id: true, + name: true, + org_affiliations: true, + cve_program_org_membership: true, + created: true, + created_by: true, + last_updated: true, + deactivation_date: true, + last_active: true + } + } + ] +} + +module.exports = { + ALL_USERS: getAllUsers, + SINGLE_USER: getUser, + CREATE_USER: createUser, + UPDATE_USER: updateUser, + DELETE_USER: deleteUser +} diff --git a/src/controller/registry-user.controller/registry-user.middleware.js b/src/controller/registry-user.controller/registry-user.middleware.js new file mode 100644 index 000000000..2a9f02983 --- /dev/null +++ b/src/controller/registry-user.controller/registry-user.middleware.js @@ -0,0 +1,30 @@ +const utils = require('../../utils/utils') + +function parsePostParams (req, res, next) { + utils.reqCtxMapping(req, 'body', []) + utils.reqCtxMapping(req, 'params', ['identifier']) + utils.reqCtxMapping(req, 'query', [ + 'new_user_id', + 'name.first', 'name.last', 'name.middle', 'name.suffix', + 'org_affiliations.add', 'org_affiliations.remove', + 'cve_program_org_membership.add', 'cve_program_org_membership.remove' + ]) + next() +} + +function parseGetParams (req, res, next) { + utils.reqCtxMapping(req, 'params', ['identifier']) + utils.reqCtxMapping(req, 'query', ['page']) + next() +} + +function parseDeleteParams (req, res, next) { + utils.reqCtxMapping(req, 'params', ['identifier']) + next() +} + +module.exports = { + parsePostParams, + parseGetParams, + parseDeleteParams +} \ No newline at end of file diff --git a/src/controller/schemas.controller/index.js b/src/controller/schemas.controller/index.js index fe9cdd2b9..0d86380aa 100644 --- a/src/controller/schemas.controller/index.js +++ b/src/controller/schemas.controller/index.js @@ -49,4 +49,10 @@ router.get('/user/list-users-response.json', controller.getListUsersSchema) router.get('/user/reset-secret-response.json', controller.getResetSecretResponseSchema) router.get('/user/update-user-response.json', controller.getUpdateUserResponseSchema) +// Schemas relating to Registry-Org +router.get('/registry-org/get-registry-org-response.json', controller.getRegistryOrgResponseSchema) + +// Schemas relating to Registry-User +router.get('/registry-user/get-registry-users-response.json', controller.getRegistryUserResponseSchema) + module.exports = router diff --git a/src/controller/schemas.controller/schemas.controller.js b/src/controller/schemas.controller/schemas.controller.js index caf97cd28..2a87eb399 100644 --- a/src/controller/schemas.controller/schemas.controller.js +++ b/src/controller/schemas.controller/schemas.controller.js @@ -228,6 +228,18 @@ async function getCveCountResponseSchema (req, res) { res.status(200) } +async function getRegistryOrgResponseSchema (req, res) { + const registryOrgResponseSchema = require('../../../schemas/registry-org/get-registry-org-response.json') + res.json(registryOrgResponseSchema) + res.status(200) +} + +async function getRegistryUserResponseSchema (req, res) { + const registryUserResponseSchema = require('../../../schemas/registry-user/get-registry-users-response.json') + res.json(registryUserResponseSchema) + res.status(200) +} + module.exports = { getBadRequestSchema: getBadRequestSchema, getCreateCveRecordResponseSchema: getCreateCveRecordResponseSchema, @@ -265,5 +277,7 @@ module.exports = { getAdpFullSchema: getAdpFullSchema, getCnaSecretariatFullSchema: getCnaSecretariatFullSchema, getCnaMinSchema: getCnaMinSchema, - getCveCountResponseSchema: getCveCountResponseSchema + getCveCountResponseSchema: getCveCountResponseSchema, + getRegistryOrgResponseSchema: getRegistryOrgResponseSchema, + getRegistryUserResponseSchema: getRegistryUserResponseSchema } diff --git a/src/controller/user.controller/index.js b/src/controller/user.controller/index.js index 5d153266b..9a768ef72 100644 --- a/src/controller/user.controller/index.js +++ b/src/controller/user.controller/index.js @@ -1,7 +1,7 @@ const express = require('express') const router = express.Router() const mw = require('../../middleware/middleware') -const { query } = require('express-validator') +const { query, param } = require('express-validator') const controller = require('./user.controller') const { parseGetParams, parseError } = require('./user.middleware') const getConstants = require('../../constants').getConstants @@ -74,8 +74,9 @@ router.get('/users', */ mw.validateUser, mw.onlySecretariat, + param(['registry']).optional().isBoolean(), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['page', 'registry']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), parseError, parseGetParams, controller.ALL_USERS) diff --git a/src/controller/user.controller/user.controller.js b/src/controller/user.controller/user.controller.js index fb4429ef6..44c435827 100644 --- a/src/controller/user.controller/user.controller.js +++ b/src/controller/user.controller/user.controller.js @@ -1,3 +1,4 @@ + require('dotenv').config() const logger = require('../../middleware/logger') const getConstants = require('../../constants').getConstants @@ -9,6 +10,7 @@ const getConstants = require('../../constants').getConstants async function getAllUsers (req, res, next) { try { const CONSTANTS = getConstants() + const isRegistry = req.query.registry === 'true' // temporary measure to allow tests to work after fixing #920 // tests required changing the global limit to force pagination @@ -19,9 +21,9 @@ async function getAllUsers (req, res, next) { const options = CONSTANTS.PAGINATOR_OPTIONS options.sort = { short_name: 'asc' } options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value - const repo = req.ctx.repositories.getUserRepository() + const repo = isRegistry ? req.ctx.repositories.getRegistryUserRepository() : req.ctx.repositories.getUserRepository() - const agt = setAggregateUserObj({}) + const agt = isRegistry ? setAggregateRegistryUserObj({}) : setAggregateUserObj({}) const pg = await repo.aggregatePaginate(agt, options) const payload = { users: pg.itemsList } @@ -41,6 +43,29 @@ async function getAllUsers (req, res, next) { } } +function setAggregateRegistryUserObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + user_id: true, + name: true, + org_affiliations: true, + cve_program_org_membership: true, + created: true, + created_by: true, + last_updated: true, + deactivation_date: true, + last_active: true + } + } + ] +} + function setAggregateUserObj (query) { return [ { diff --git a/src/controller/user.controller/user.middleware.js b/src/controller/user.controller/user.middleware.js index 95a900313..e9477fb70 100644 --- a/src/controller/user.controller/user.middleware.js +++ b/src/controller/user.controller/user.middleware.js @@ -4,7 +4,7 @@ const error = new errors.UserControllerError() const utils = require('../../utils/utils') function parseGetParams (req, res, next) { - utils.reqCtxMapping(req, 'query', ['page']) + utils.reqCtxMapping(req, 'query', ['page', 'registry']) next() } diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 5bfb60726..c929ea3d2 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -87,8 +87,17 @@ async function validateUser (req, res, next) { const org = req.ctx.org const user = req.ctx.user const key = req.ctx.key - const userRepo = req.ctx.repositories.getUserRepository() - const orgRepo = req.ctx.repositories.getOrgRepository() + let userRepo = null + let orgRepo = null + const useRegistry = req.query.registry === 'true' + if (useRegistry) { + userRepo = req.ctx.repositories.getRegistryUserRepository() + orgRepo = req.ctx.repositories.getRegistryOrgRepository() + } else { + userRepo = req.ctx.repositories.getUserRepository() + orgRepo = req.ctx.repositories.getOrgRepository() + } + const CONSTANTS = getConstants() try { @@ -117,7 +126,21 @@ async function validateUser (req, res, next) { return res.status(401).json(error.unauthorized()) } - if (!result.active) { + let activeInOrg = false + if (useRegistry) { + // Check if user has active status organization's registry org membership list + for (var organization of result.cve_program_org_membership) { + if (organization.program_org === orgUUID) { + if (organization.status === 'active' || organization.status === 'true') { + activeInOrg = true + } + break + } + } + } + + if ((!useRegistry && !result.active) || + (useRegistry && !activeInOrg)) { logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User deactivated. Authentication failed for ' + user })) return res.status(401).json(error.unauthorized()) } @@ -160,10 +183,35 @@ async function onlySecretariatOrBulkDownload (req, res, next) { } } +async function onlySecretariatUserRegistry (req, res, next) { + const org = req.ctx.org + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + const CONSTANTS = getConstants() + + try { + const isSec = await registryOrgRepo.isSecretariat(org) + if (!isSec) { + logger.info({ uuid: req.ctx.uuid, message: org + ' is NOT a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT }) + return res.status(403).json(error.secretariatOnly()) + } + logger.info({ uuid: req.ctx.uuid, message: 'Confirmed ' + org + 'as a Secretariat' }) + next() + } catch (err) { + next(err) + } +} + // Checks that the requester belongs to an org that has the 'SECRETARIAT' role + async function onlySecretariat (req, res, next) { const org = req.ctx.org - const orgRepo = req.ctx.repositories.getOrgRepository() + let orgRepo = null + const useRegistry = req.query.registry === 'true' + if (useRegistry) { + orgRepo = req.ctx.repositories.getRegistryOrgRepository() + } else { + orgRepo = req.ctx.repositories.getOrgRepository() + } const CONSTANTS = getConstants() try { @@ -445,6 +493,10 @@ function isFlatStringArray (val) { return true } +function isCveProgramOrgMembershipObject (val) { + console.log(val) +} + /** * Recursively casts to strings and upper-cases all items in array * @@ -486,6 +538,7 @@ module.exports = { onlySecretariat, onlySecretariatOrBulkDownload, onlySecretariatOrAdmin, + onlySecretariatUserRegistry, onlyCnas, onlyAdps, onlyOrgWithPartnerRole, @@ -497,6 +550,7 @@ module.exports = { validateJsonSyntax, rateLimiter: limiter, isFlatStringArray, + isCveProgramOrgMembershipObject, toUpperCaseArray, containsNoInvalidCharacters, trimJSONWhitespace diff --git a/src/model/registry-org.js b/src/model/registry-org.js new file mode 100644 index 000000000..b7b046e61 --- /dev/null +++ b/src/model/registry-org.js @@ -0,0 +1,120 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose +const aggregatePaginate = require('mongoose-aggregate-paginate-v2') +const MongoPaging = require('mongo-cursor-pagination') + +const schema = { + _id: false, + UUID: String, + long_name: String, + short_name: String, + aliases: [String], + cve_program_org_function: { + type: String, + enum: ['Top Level Root', 'Root', 'CNA', 'CNA-LR', 'Secretariat', 'Board', 'AWG', 'TWG', 'SPWG', 'Bulk Download', 'ADP'] + }, + authority: { + active_roles: [String] + }, + reports_to: String, + oversees: [String], + root_or_tlr: Boolean, + users: [String], + charter_or_scope: String, + disclosure_policy: String, + product_list: String, + soft_quota: Number, + hard_quota: Number, + contact_info: { + additional_contact_users: [String], + poc: String, + poc_email: String, + poc_phone: String, + admins: [String], + org_email: String, + website: String + }, + in_use: Boolean, + created: Date, + last_updated: Date +} + +const orgPrivate = '-_id -soft_quota -hard_quota -contact_info.admins -in_use -created -last_updated -__v' +const orgSecretariat = '' +const RegistryOrgSchema = new mongoose.Schema(schema, { collection: 'RegistryOrg', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }) + +RegistryOrgSchema.query.byShortName = function (shortName) { + return this.where({ short_name: shortName }) +} + +RegistryOrgSchema.query.byUUID = function (uuid) { + return this.where({ UUID: uuid }) +} + +RegistryOrgSchema.statics.populateOverseesAndReportsTo = async function (items) { // Assuming the model name is 'RegistryOrg' + for (const item of items) { + if (item.oversees.length > 0) { + const populatedOversees = await Promise.all( + item.oversees.map(async (uuid) => { + const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate) + return org ? org.toObject() : uuid // Return the org object if found, otherwise return the UUID + }) + ) + item.oversees = populatedOversees + } + if (item.reports_to) { + const org = await RegistryOrg.findOne({ UUID: item.reports_to }).select(orgPrivate) + item.reports_to = org ? org.toObject() : item.reports_to // Return the org object if found, otherwise return the UUID + } + } + + return this +} + +RegistryOrgSchema.statics.populateOrgAffiliations = async function (items) { // Assuming the model name is 'RegistryOrg' + for (const item of items) { + if (item.org_affiliations.length > 0) { + const populatedOrgs = await Promise.all( + item.org_affiliations.map(async ({ org_id: uuid, ...orgMeta }) => { + const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate) + return { + org: org ? org.toObject() : uuid, // Return the org object if found, otherwise return the UUID + ...orgMeta + } + }) + ) + item.org_affiliations = populatedOrgs + } + } + + return this +} + +RegistryOrgSchema.statics.populateCVEProgramOrgMembership = async function (items) { // Assuming the model name is 'RegistryOrg' + for (const item of items) { + if (item.cve_program_org_membership.length > 0) { + const populatedOrgs = await Promise.all( + item.cve_program_org_membership.map(async ({ program_org: uuid, ...orgMeta }) => { + const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate) + return { + org: org ? org.toObject() : uuid, // Return the org object if found, otherwise return the UUID + ...orgMeta + } + }) + ) + item.cve_program_org_membership = populatedOrgs + } + } + + return this +} + +RegistryOrgSchema.index({ UUID: 1 }) +RegistryOrgSchema.index({ 'authority.active_roles': 1 }) + +RegistryOrgSchema.plugin(aggregatePaginate) + +// Cursor pagination +RegistryOrgSchema.plugin(MongoPaging.mongoosePlugin) +const RegistryOrg = mongoose.model('RegistryOrg', RegistryOrgSchema) +module.exports = RegistryOrg diff --git a/src/model/registry-user.js b/src/model/registry-user.js new file mode 100644 index 000000000..5f264e761 --- /dev/null +++ b/src/model/registry-user.js @@ -0,0 +1,112 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose +const aggregatePaginate = require('mongoose-aggregate-paginate-v2') +const MongoPaging = require('mongo-cursor-pagination') + +const schema = { + _id: false, + UUID: String, + user_id: String, + secret: String, + name: { + first: String, + last: String, + middle: String, + suffix: String + }, + org_affiliations: [{ + org_id: String, + email: String, + phone: String + }], + cve_program_org_membership: [{ + program_org: String, + role: { + type: String, + enum: ['Chair', 'Member', 'Admin'] + }, + status: { + type: String, + enum: ['active', 'inactive'] + } + }], + created: Date, + created_by: String, + last_updated: Date, + deactivation_date: Date, + last_active: Date +} + +const userPrivate = '-secret -_id -org_affiliations._id -cve_program_org_membership._id -created_by -created -last_updated -last_active -__v' +const userSecretariat = '-secret' +const RegistryUserSchema = new mongoose.Schema(schema, { collection: 'RegistryUser', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }) + +RegistryUserSchema.query.byUserID = function (userID) { + return this.where({ user_id: userID }) +} + +RegistryUserSchema.query.byUUID = function (uuid) { + return this.where({ UUID: uuid }) +} + +RegistryUserSchema.query.byUserIdAndOrgUUID = function (userId, orgUUID) { + return this.where({ user_id: userId, 'org_affiliations.org_id': orgUUID }) +} + +RegistryUserSchema.statics.populateAdmins = async function (items) { // Assuming the model name is 'RegistryUser' + for (const item of items) { + if (item.contact_info && item.contact_info.admins && item.contact_info.admins.length > 0) { + const populatedAdmins = await Promise.all( + item.contact_info.admins.map(async (uuid) => { + const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields + return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID + }) + ) + item.contact_info.admins = populatedAdmins + } + } + + return this +} + +RegistryUserSchema.statics.populateUsers = async function (items) { // Assuming the model name is 'RegistryUser' + for (const item of items) { + if (item.users && item.users.length > 0) { + const populatedUsers = await Promise.all( + item.users.map(async (uuid) => { + const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields + return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID + }) + ) + item.users = populatedUsers + } + } + + return this +} + +RegistryUserSchema.statics.populateAdditionalContactUsers = async function (items) { // Assuming the model name is 'RegistryUser' + for (const item of items) { + if (item.contact_info && item.contact_info.additional_contact_users && item.contact_info.additional_contact_users.length > 0) { + const populatedUsers = await Promise.all( + item.contact_info.additional_contact_users.map(async (uuid) => { + const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields + return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID + }) + ) + item.users = populatedUsers + } + } + + return this +} + +RegistryUserSchema.index({ UUID: 1 }) +RegistryUserSchema.index({ user_id: 1 }) + +RegistryUserSchema.plugin(aggregatePaginate) + +// Cursor pagination +RegistryUserSchema.plugin(MongoPaging.mongoosePlugin) +const RegistryUser = mongoose.model('RegistryUser', RegistryUserSchema) +module.exports = RegistryUser diff --git a/src/repositories/baseRepository.js b/src/repositories/baseRepository.js index 1e468faeb..1a179d157 100644 --- a/src/repositories/baseRepository.js +++ b/src/repositories/baseRepository.js @@ -11,8 +11,23 @@ class BaseRepository { } } - async aggregate (aggregation) { - return this.collection.aggregate(aggregation) + async aggregate (pipeline, options = {}) { + const aggQuery = this.collection.aggregate(pipeline) + + // Check if a session is provided in the options and apply it + if (options.session) { + aggQuery.session(options.session) + } + + // You can also pass other Mongoose aggregate options if needed from 'options' + // if (options.readConcern) { + // aggQuery.readConcern(options.readConcern); + // } + // if (options.collation) { + // aggQuery.collation(options.collation); + // } + + return aggQuery.exec() } async aggregatePaginate (aggregation, options) { diff --git a/src/repositories/orgRepository.js b/src/repositories/orgRepository.js index 84d47fb7c..acd6c0715 100644 --- a/src/repositories/orgRepository.js +++ b/src/repositories/orgRepository.js @@ -7,20 +7,24 @@ class OrgRepository extends BaseRepository { super(Org) } - async findOneByShortName (shortName) { - return this.collection.findOne().byShortName(shortName) + async findOneByShortName (shortName, options = {}) { + const query = { short_name: shortName } + return this.collection.findOne(query, null, options) } async findOneByUUID (UUID) { return this.collection.findOne().byUUID(UUID) } - async getOrgUUID (shortName) { - return utils.getOrgUUID(shortName) + async getOrgUUID (shortName, options = {}) { + return utils.getOrgUUID(shortName, false, options) } - async updateByOrgUUID (orgUUID, org, options = {}) { - return this.collection.findOneAndUpdate().byUUID(orgUUID).updateOne(org).setOptions(options) + async updateByOrgUUID (orgUUID, updateData, executeOptions = {}) { + // The filter to find the document + const filter = { UUID: orgUUID } + const updatePayload = { $set: updateData } + return this.collection.findOneAndUpdate(filter, updatePayload, executeOptions) } async isSecretariat (shortName) { diff --git a/src/repositories/registryOrgRepository.js b/src/repositories/registryOrgRepository.js new file mode 100644 index 000000000..e2bc12fbc --- /dev/null +++ b/src/repositories/registryOrgRepository.js @@ -0,0 +1,44 @@ +const BaseRepository = require('./baseRepository') +const RegistryOrg = require('../model/registry-org') +const utils = require('../utils/utils') + +class RegistryOrgRepository extends BaseRepository { + constructor () { + super(RegistryOrg) + } + + async findOneByShortName (shortName, options = {}) { + const query = { short_name: shortName } + // We are returning the whole object here, so no projection is needed + return this.collection.findOne(query, null, options) + } + + async findOneByUUID (UUID) { + return this.collection.findOne().byUUID(UUID) + } + + async getOrgUUID (shortName, options = {}) { + return utils.getOrgUUID(shortName, true, options) // use registryOrgRepository to find org UUID + } + + async getAllOrgs () { + return this.collection.find() + } + + async isSecretariat (shortName) { + return utils.isSecretariat(shortName, true) + } + + async updateByUUID (uuid, org, options = {}) { + // The filter to find the document + const filter = { UUID: uuid } + const updatePayload = { $set: org } + return this.collection.findOneAndUpdate(filter, updatePayload, options) + } + + async deleteByUUID (uuid) { + return this.collection.deleteOne({ UUID: uuid }) + } +} + +module.exports = RegistryOrgRepository diff --git a/src/repositories/registryUserRepository.js b/src/repositories/registryUserRepository.js new file mode 100644 index 000000000..6985b326f --- /dev/null +++ b/src/repositories/registryUserRepository.js @@ -0,0 +1,50 @@ +const BaseRepository = require('./baseRepository') +const RegistryUser = require('../model/registry-user') +const utils = require('../utils/utils') + +class RegistryUserRepository extends BaseRepository { + constructor () { + super(RegistryUser) + } + + async getUserUUID (username, orgUUID, options = {}) { + return utils.getUserUUID(username, orgUUID, true, options) + } + + async findOneByUUID (UUID) { + return this.collection.findOne().byUUID(UUID) + } + + async getAllUsers () { + return this.collection.find() + } + + async isSecretariat (org, options = {}) { + return utils.isSecretariat(org, true, options) + } + + async isAdmin (username, orgShortname, options = {}) { + return utils.isAdmin(username, orgShortname, true, options) + } + + async updateByUUID (uuid, user, options = {}) { + return this.collection.findOneAndUpdate().byUUID(uuid).updateOne(user).setOptions(options) + } + + async findOneByUserNameAndOrgUUID (userName, orgUUID, projection = null, options = {}) { + const query = { user_id: userName, 'org_affiliations.org_id': orgUUID } + return this.collection.findOne(query, projection, options) + } + + async updateByUserNameAndOrgUUID (userName, orgUUID, user, options = {}) { + const filter = { user_id: userName, 'org_affiliations.org_id': orgUUID } + const updatePayload = { $set: user } + return this.collection.findOneAndUpdate(filter, updatePayload, options) + } + + async deleteByUUID (uuid) { + return this.collection.deleteOne({ UUID: uuid }) + } +} + +module.exports = RegistryUserRepository diff --git a/src/repositories/repositoryFactory.js b/src/repositories/repositoryFactory.js index 2fb251d7b..5ba558a69 100644 --- a/src/repositories/repositoryFactory.js +++ b/src/repositories/repositoryFactory.js @@ -3,6 +3,8 @@ const CveRepository = require('./cveRepository') const CveIdRepository = require('./cveIdRepository') const CveIdRangeRepository = require('./cveIdRangeRepository') const UserRepository = require('./userRepository') +const RegistryUserRepository = require('./registryUserRepository') +const RegistryOrgRepository = require('./registryOrgRepository') class RepositoryFactory { getOrgRepository () { @@ -29,6 +31,16 @@ class RepositoryFactory { const repo = new UserRepository() return repo } + + getRegistryUserRepository () { + const repo = new RegistryUserRepository() + return repo + } + + getRegistryOrgRepository () { + const repo = new RegistryOrgRepository() + return repo + } } module.exports = RepositoryFactory diff --git a/src/repositories/userRepository.js b/src/repositories/userRepository.js index 4aa255a42..1be45a610 100644 --- a/src/repositories/userRepository.js +++ b/src/repositories/userRepository.js @@ -7,12 +7,12 @@ class UserRepository extends BaseRepository { super(User) } - async getUserUUID (userName, orgUUID) { - return utils.getUserUUID(userName, orgUUID) + async getUserUUID (userName, orgUUID, options = {}) { + return utils.getUserUUID(userName, orgUUID, options) } - async isAdmin (username, shortname) { - return utils.isAdmin(username, shortname) + async isAdmin (username, shortname, options = {}) { + return utils.isAdmin(username, shortname, false, options) } async isAdminUUID (username, orgUUID) { @@ -27,12 +27,15 @@ class UserRepository extends BaseRepository { return this.collection.find().byOrgUUID(orgUUID).countDocuments().exec() } - async findOneByUserNameAndOrgUUID (userName, orgUUID) { - return this.collection.findOne().byUserNameAndOrgUUID(userName, orgUUID) + async findOneByUserNameAndOrgUUID (userName, orgUUID, projection = null, options = {}) { + const query = { username: userName, org_UUID: orgUUID } + return this.collection.findOne(query, projection, options) } async updateByUserNameAndOrgUUID (username, orgUUID, user, options = {}) { - return this.collection.findOneAndUpdate().byUserNameAndOrgUUID(username, orgUUID).updateOne(user).setOptions(options) + const filter = { username: username, org_UUID: orgUUID } + const updatePayload = { $set: user } + return this.collection.findOneAndUpdate(filter, updatePayload, options) } async getAllUsers () { diff --git a/src/routes.config.js b/src/routes.config.js index e1fc27835..7ce0fb508 100644 --- a/src/routes.config.js +++ b/src/routes.config.js @@ -7,6 +7,8 @@ const CveIdController = require('./controller/cve-id.controller') const SchemasController = require('./controller/schemas.controller') const SystemController = require('./controller/system.controller') const UserController = require('./controller/user.controller') +const RegistryUserController = require('./controller/registry-user.controller') +const RegistryOrgController = require('./controller/registry-org.controller') var options = { swaggerOptions: { @@ -30,6 +32,8 @@ module.exports = async function configureRoutes (app) { app.use('/api/', CveIdController) app.use('/api/', SystemController) app.use('/api/', UserController) + app.use('/api/', RegistryUserController) + app.use('/api/', RegistryOrgController) app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification)) app.use('/api-docs', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, setupOptions)) app.use('/schemas/', SchemasController) diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 22b3ee9f8..1b1422d75 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -16,6 +16,8 @@ const CveId = require('../model/cve-id') const Cve = require('../model/cve') const Org = require('../model/org') const User = require('../model/user') +const RegistryOrg = require('../model/registry-org') +const RegistryUser = require('../model/registry-user') const error = new errors.IDRError() @@ -24,7 +26,9 @@ const populateTheseCollections = { 'Cve-Id-Range': CveIdRange, 'Cve-Id': CveId, User: User, - Org: Org + Org: Org, + RegistryOrg: RegistryOrg, + RegistryUser: RegistryUser } const indexesToCreate = { @@ -89,14 +93,18 @@ db.once('open', async () => { names.push(collection.name) }) - if (!names.includes('Cve-Id-Range') && !names.includes('Cve-Id') && !names.includes('Cve') && - !names.includes('Org') && !names.includes('User')) { + if (!names.includes('Cve-Id-Range') && !names.includes('Cve-Id') && !names.includes('Cve') && !names.includes('Org') && !names.includes('User')) { // Org await dataUtils.populateCollection( './datadump/pre-population/orgs.json', Org, dataUtils.newOrgTransform ) + await dataUtils.populateCollection( + './datadump/pre-population/registry-orgs.json', + RegistryOrg + ) + // User, depends on Org const hash = await dataUtils.preprocessUserSecrets() await dataUtils.populateCollection( @@ -104,6 +112,12 @@ db.once('open', async () => { User, dataUtils.newUserTransform, hash ) + const registryUserHash = await dataUtils.preprocessUserSecrets() + await dataUtils.populateCollection( + './datadump/pre-population/registry-users.json', + RegistryUser, dataUtils.newRegistryUserTransform, registryUserHash + ) + const populatePromises = [] // CVE ID Range diff --git a/src/scripts/set-replica-set.js b/src/scripts/set-replica-set.js new file mode 100644 index 000000000..cf460021c --- /dev/null +++ b/src/scripts/set-replica-set.js @@ -0,0 +1,99 @@ +// Filename: initMongoReplicaSet.js +// Purpose: Initiates a MongoDB replica set on a mongod instance +// that was started with the --replSet option. +// Uses directConnection=true for the initial connection to simplify +// connecting to a node that is not yet part of an initialized replica set. + +const { MongoClient } = require('mongodb') + +// Configuration +const MONGODB_HOST_IP = '127.0.0.1' // Explicitly use 127.0.0.1 +const MONGODB_PORT = '27017' +// Added ?directConnection=true to the URI +const MONGODB_URI = `mongodb://${MONGODB_HOST_IP}:${MONGODB_PORT}/admin?directConnection=true` +const REPLICA_SET_NAME = 'rs0' // Must match the --replSet name used when starting mongod +const HOST_ADDRESS = `${MONGODB_HOST_IP}:${MONGODB_PORT}` // The address of this mongod instance +const SERVER_SELECTION_TIMEOUT_MS = 35000 // Slightly increased timeout + +async function initiateReplicaSet () { + // serverSelectionTimeoutMS might be less relevant with directConnection=true, but kept for consistency + const client = new MongoClient(MONGODB_URI, { serverSelectionTimeoutMS: SERVER_SELECTION_TIMEOUT_MS }) + + try { + console.log(`Attempting to connect to MongoDB at ${MONGODB_URI} ...`) + await client.connect() + console.log('Successfully connected to MongoDB (using directConnection).') + + const adminDb = client.db('admin') // Ensure we are using the admin database + + // Check current replica set status + let status + try { + console.log('Checking replica set status...') + // With directConnection, replSetGetStatus might behave differently or even fail if not on a replSet member. + // However, our mongod IS configured as a replSet member, just not initiated. + status = await adminDb.command({ replSetGetStatus: 1 }) + + if (status.ok && status.set === REPLICA_SET_NAME && status.myState === 1) { + console.log(`Replica set '${REPLICA_SET_NAME}' is already initialized and this node is PRIMARY.`) + return + } + if (status.ok && status.members && status.members.length > 0) { + console.log(`Replica set '${status.set || REPLICA_SET_NAME}' seems to be already configured or in a specific state.`) + console.log('Current status:', JSON.stringify(status, null, 2)) + return + } + if (status.ok === 0 && status.codeName !== 'NotYetInitialized' && !status.errmsg?.includes('no replset config')) { + console.warn('Replica set status check returned an unexpected error:', JSON.stringify(status, null, 2)) + } + } catch (err) { + if (err.codeName === 'NotYetInitialized' || err.message.includes('no replset config') || err.message.includes('NotYetInitialized')) { + console.log('Replica set not yet initialized (as expected from rs.status() in mongosh). Proceeding with initialization.') + } else if (err.code === 94 || err.message.includes('No replica set name has been specified')) { // Error code for replSetGetStatus on non-replset node + console.log('replSetGetStatus failed, likely because directConnection is on and it is not fully initialized. This is okay if mongod was started with --replSet. Proceeding with initiation attempt.') + } else { + console.warn(`Warning during replica set status check: ${err.message}. Attempting initialization anyway.`) + console.log('Error details:', JSON.stringify(err, null, 2)) + } + } + + // Define the replica set configuration + const replicaSetConfig = { + _id: REPLICA_SET_NAME, + members: [ + { _id: 0, host: HOST_ADDRESS } + ] + } + + console.log(`Attempting to initiate replica set '${REPLICA_SET_NAME}' with config:`, JSON.stringify(replicaSetConfig, null, 2)) + + const result = await adminDb.command({ replSetInitiate: replicaSetConfig }) + + if (result.ok === 1) { + console.log(`Replica set '${REPLICA_SET_NAME}' initiated successfully!`) + console.log('It might take a few moments for the node to become PRIMARY.') + console.log('You can verify with `mongosh` and `rs.status()`.') + } else { + console.error('Failed to initiate replica set.', result) + if (result.codeName === 'InvalidReplicaSetConfig') { + console.error('Detail: The replica set configuration was invalid. This can happen if the host address is not resolvable from the perspective of the mongod server itself.') + } else if (result.codeName === 'AlreadyInitialized') { + console.log(`Replica set '${REPLICA_SET_NAME}' is already initialized.`) + } + } + } catch (error) { + console.error('An error occurred during the process:', error) + if (error.message && error.message.includes('already initialized')) { + console.log(`It seems the replica set '${REPLICA_SET_NAME}' is already initialized.`) + } else if (error.codeName === 'ConfigurationInProgress') { + console.log('Replica set configuration is already in progress or node is recovering. Try again in a moment.') + } + } finally { + if (client) { + await client.close() + console.log('MongoDB connection closed.') + } + } +} + +initiateReplicaSet() diff --git a/src/swagger.js b/src/swagger.js index 0aaa44171..55ad59a3e 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -5,7 +5,9 @@ const endpointsFiles = [ 'src/controller/cve.controller/index.js', 'src/controller/org.controller/index.js', 'src/controller/user.controller/index.js', - 'src/controller/system.controller/index.js' + 'src/controller/system.controller/index.js', + 'src/controller/registry-org.controller/index.js', + 'src/controller/registry-user.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/data.js b/src/utils/data.js index 4352c822b..0fdc10bab 100644 --- a/src/utils/data.js +++ b/src/utils/data.js @@ -86,6 +86,14 @@ async function newUserTransform (user, hash) { return user } +async function newRegistryUserTransform (user, hash) { + // shared secret key in development environments + if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { + user.secret = hash + } + return user +} + async function newCveIdTransform (cveId) { const tmpRequestingCnaUUID = await utils.getOrgUUID(cveId.requested_by.cna) const tmpOwningCnaUUID = await utils.getOrgUUID(cveId.owning_cna) @@ -162,6 +170,7 @@ module.exports = { newCveIdTransform, newOrgTransform, newUserTransform, + newRegistryUserTransform, newCveTransform, populateCollection, preprocessUserSecrets diff --git a/src/utils/utils.js b/src/utils/utils.js index 1ef6d63d5..47dfbabd7 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,32 +1,69 @@ const Org = require('../model/org') const User = require('../model/user') + +const RegistryOrg = require('../model/registry-org') +const RegistryUser = require('../model/registry-user') + const getConstants = require('../constants').getConstants const _ = require('lodash') const { DateTime } = require('luxon') -async function getOrgUUID (shortName) { - const org = await Org.findOne().byShortName(shortName) - let result = null - if (org) { - result = org.UUID - } - return result +async function getOrgUUID (shortName, useRegistry = false, options = {}) { + const ModelToQuery = useRegistry ? RegistryOrg : Org + const query = { short_name: shortName } + const projection = 'UUID' // We only need the UUID field + + // It's often good practice to use .lean() for read-only operations + // if you don't need full Mongoose documents. + + const executionOptions = { ...options } + if (executionOptions.lean === undefined) executionOptions.lean = true + + const orgDocument = await ModelToQuery.findOne(query, projection, executionOptions) + + return orgDocument ? orgDocument.UUID : null } -async function getUserUUID (userName, orgUUID) { - const user = await User.findOne().byUserNameAndOrgUUID(userName, orgUUID) - let result = null - if (user) { - result = user.UUID +async function getUserUUID (userIdentifier, orgUUID, useRegistry = false, options = {}) { + const ModelToQuery = useRegistry ? RegistryUser : User + let query + + if (useRegistry) { + // For RegistryUser, query by user_id and check within the org_affiliations array + query = { + user_id: userIdentifier, // Matches the 'user_id' field in RegistryUser schema + 'org_affiliations.org_id': orgUUID // Uses dot notation to query the array + } + } else { + // For User, query by username and org_UUID + query = { + username: userIdentifier, // Matches the 'username' field in User schema + org_UUID: orgUUID // Matches the 'org_UUID' field in User schema + } } - return result + + const projection = 'UUID' // We only need the user's UUID field + const executionOptions = { ...options } + if (executionOptions.lean === undefined) executionOptions.lean = true + + const userDocument = await ModelToQuery.findOne(query, projection, executionOptions) + + return userDocument ? userDocument.UUID : null } -async function isSecretariat (shortName) { +async function isSecretariat (shortName, useRegistry = false, options = {}) { let result = false + let orgUUID = null + let secretariats = [] + const CONSTANTS = getConstants() - const orgUUID = await getOrgUUID(shortName) // may be null if org does not exists - const secretariats = await Org.find({ 'authority.active_roles': { $in: [CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT] } }) + if (useRegistry) { + orgUUID = await getOrgUUID(shortName, useRegistry, options) // may be null if org does not exists + secretariats = await RegistryOrg.find({ 'authority.active_roles': { $in: [CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT] } }) + } else { + orgUUID = await getOrgUUID(shortName, false, options) // may be null if org does not exists + secretariats = await Org.find({ 'authority.active_roles': { $in: [CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT] } }) + } if (orgUUID) { secretariats.forEach((obj) => { @@ -72,13 +109,13 @@ async function isBulkDownload (shortName) { return result // org does not have bulk download as a role } -async function isAdmin (requesterUsername, requesterShortName) { +async function isAdmin (requesterUsername, requesterShortName, isRegistry = false, options = {}) { let result = false const CONSTANTS = getConstants() - const requesterOrgUUID = await getOrgUUID(requesterShortName) // may be null if org does not exists + const requesterOrgUUID = await getOrgUUID(requesterShortName, isRegistry, options) // may be null if org does not exists if (requesterOrgUUID) { - const user = await User.findOne().byUserNameAndOrgUUID(requesterUsername, requesterOrgUUID) + const user = isRegistry ? await RegistryUser.findOne().byUserNameAndOrgUUID(requesterUsername, requesterShortName) : await User.findOne().byUserNameAndOrgUUID(requesterUsername, requesterShortName) if (user) { result = user.authority.active_roles.includes(CONSTANTS.USER_ROLE_ENUM.ADMIN)