diff --git a/api-docs/openapi.json b/api-docs/openapi.json index f9fbbb5bd..84fe59bcf 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "2.8.3", + "version": "2.8.4", "title": "CVE Services API", "description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:

CVE data is to be in the JSON 5.2 CVE Record format. Details of the JSON 5.2 schema are located here.

Contact the CVE Services team", "contact": { @@ -1893,14 +1893,14 @@ } } }, - "/registry/org": { + "/org": { "get": { "tags": [ - "Registry Organization" + "Organization" ], - "summary": "Retrieves all registry organizations (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all registry organizations

", - "operationId": "registryOrgAll", + "summary": "Retrieves all organizations (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all organizations

", + "operationId": "orgAll", "parameters": [ { "$ref": "#/components/parameters/pageQuery" @@ -1917,11 +1917,18 @@ ], "responses": { "200": { - "description": "Returns information about all registry organizations, along with pagination fields if results span multiple pages of data", + "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/list-registry-orgs-response.json" + "oneOf": [ + { + "$ref": "../schemas/org/list-orgs-response.json" + }, + { + "$ref": "../schemas/registry-org/list-registry-orgs-response.json" + } + ] } } } @@ -1980,10 +1987,10 @@ }, "post": { "tags": [ - "Registry Organization" + "Organization" ], - "summary": "Creates an organization (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates a new organization

", + "summary": "Creates an organization as specified in the request body (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates an organization

", "operationId": "orgCreateSingle", "parameters": [ { @@ -1998,11 +2005,18 @@ ], "responses": { "200": { - "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data", + "description": "Returns information about the organization created", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/list-registry-orgs-response.json" + "oneOf": [ + { + "$ref": "../schemas/org/create-org-response.json" + }, + { + "$ref": "../schemas/registry-org/create-registry-org-response.json" + } + ] } } } @@ -2063,54 +2077,30 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "../schemas/registry-org/SecretariatOrg.json" - }, - { - "$ref": "../schemas/registry-org/CNAOrg.json" - }, - { - "$ref": "../schemas/registry-org/ADPOrg.json" - }, - { - "$ref": "../schemas/registry-org/BulkDownloadOrg.json" - } - ] - }, - "example": { - "short_name": "fake_company", - "long_name": "Fake Company", - "id_quota": 1000, - "authority": [ - "CNA" - ] + "$ref": "../schemas/org/create-org-request.json" } } } } } }, - "/registry/org/{shortname}/users": { + "/org/{identifier}": { "get": { "tags": [ - "Registry User" + "Organization" ], - "summary": "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about users in the same organization

Secretariat: Retrieves all user information for any organization

", - "operationId": "userOrgAll", + "summary": "Retrieves information about the organization specified by short name or UUID (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves organization record for the specified shortname or UUID if it is the user's organization

Secretariat: Retrieves information about any organization

", + "operationId": "orgSingle", "parameters": [ { - "name": "shortname", + "name": "identifier", "in": "path", "required": true, "schema": { "type": "string" }, - "description": "The shortname of the organization" - }, - { - "$ref": "#/components/parameters/pageQuery" + "description": "The shortname or UUID of the organization" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -2124,33 +2114,11 @@ ], "responses": { "200": { - "description": "Returns all users for the organization, along with pagination fields if results span multiple pages of data", + "description": "Returns the organization information", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/list-registry-users-response.json" - }, - "example": { - "totalCount": 1, - "itemsPerPage": 100, - "pageCount": 1, - "currentPage": 1, - "prevPage": null, - "nextPage": null, - "users": [ - { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "role": "ADMIN", - "status": "active", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } - ] + "$ref": "../schemas/org/get-org-response.json" } } } @@ -2208,14 +2176,14 @@ } } }, - "/registry/org/{shortname}/id_quota": { - "get": { + "/org/{shortname}": { + "put": { "tags": [ - "Registry Organization" + "Organization" ], - "summary": "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

Secretariat: Retrieves the CVE ID quota for any organization

", - "operationId": "orgIdQuota", + "summary": "Updates information about the organization specified by short name (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Updates any organization's information

", + "operationId": "orgUpdateSingle", "parameters": [ { "name": "shortname", @@ -2226,6 +2194,21 @@ }, "description": "The shortname of the organization" }, + { + "$ref": "#/components/parameters/id_quota" + }, + { + "$ref": "#/components/parameters/name" + }, + { + "$ref": "#/components/parameters/newShortname" + }, + { + "$ref": "#/components/parameters/active_roles_add" + }, + { + "$ref": "#/components/parameters/active_roles_remove" + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2238,11 +2221,11 @@ ], "responses": { "200": { - "description": "Returns the CVE ID quota for an organization", + "description": "Returns information about the organization updated", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/get-registry-org-quota-response.json" + "$ref": "../schemas/org/update-org-response.json" } } } @@ -2300,47 +2283,23 @@ } } }, - "/registry/org/{identifier}": { + "/org/{shortname}/id_quota": { "get": { "tags": [ - "Registry Organization" + "Organization" ], - "summary": "Retrieves information about the registry organization specified by short name or UUID (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization

Secretariat: Retrieves information about any registry organization

", - "operationId": "registryOrgSingle", + "summary": "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

Secretariat: Retrieves the CVE ID quota for any organization

", + "operationId": "orgIdQuota", "parameters": [ { - "name": "identifier", + "name": "shortname", "in": "path", "required": true, "schema": { "type": "string" }, - "description": "The shortname or UUID of the registry organization" - }, - { - "name": "expand", - "in": "query", - "description": "Optional expanded related data. Accepted value: users.", - "required": false, - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "string" - }, - "enum": { - "type": "array", - "example": [ - "users" - ], - "items": { - "type": "string" - } - } - } - } + "description": "The shortname of the organization" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -2354,11 +2313,11 @@ ], "responses": { "200": { - "description": "Returns the registry organization information", + "description": "Returns the CVE ID quota for an organization", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/get-registry-org-response.json" + "$ref": "../schemas/org/get-org-quota-response.json" } } } @@ -2416,14 +2375,14 @@ } } }, - "/registry/org/{shortname}/user/{username}": { + "/org/{shortname}/users": { "get": { "tags": [ - "Registry User" + "Users" ], - "summary": "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about a registry user in the same organization

Secretariat: Retrieves any registry user's information

", - "operationId": "registryUserSingle", + "summary": "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about users in the same organization

Secretariat: Retrieves all user information for any organization

", + "operationId": "userOrgAll", "parameters": [ { "name": "shortname", @@ -2431,15 +2390,11 @@ "required": true, "schema": { "type": "string" - } + }, + "description": "The shortname of the organization" }, { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -2453,11 +2408,11 @@ ], "responses": { "200": { - "description": "Returns information about the specified registry user", + "description": "Returns all users for the organization, along with pagination fields if results span multiple pages of data", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/get-registry-user-response.json" + "$ref": "../schemas/user/list-users-response.json" } } } @@ -2513,14 +2468,16 @@ } } } - }, - "put": { + } + }, + "/org/{shortname}/user": { + "post": { "tags": [ - "Registry User" + "Users" ], - "summary": "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)", - "description": "

Access Control

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

Expected Behavior

Regular User: Updates the user's own information. Only name fields may be changed.

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

", - "operationId": "registryUserUpdateSingle", + "summary": "Create a user with the provided short name as the owning organization (accessible to Secretariat or target organization Admin)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Creates a user for the Admin's organization

Secretariat: Creates a user for any organization

", + "operationId": "userCreateSingle", "parameters": [ { "name": "shortname", @@ -2531,21 +2488,6 @@ }, "description": "The shortname of the organization" }, - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The username of the user" - }, - { - "$ref": "#/components/parameters/active" - }, - { - "$ref": "#/components/parameters/orgShortname" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2558,25 +2500,11 @@ ], "responses": { "200": { - "description": "Returns the updated user information", + "description": "Returns the new user information (with the secret)", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/update-registry-user-response.json" - }, - "example": { - "message": "jdoe was successfully updated.", - "updated": { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "status": "active", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } + "$ref": "../schemas/user/create-user-response.json" } } } @@ -2631,17 +2559,27 @@ } } } - } - } - }, - "/registry/org/{shortname}": { - "put": { - "tags": [ - "Registry Organization" - ], - "summary": "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

", - "operationId": "orgUpdateSingle", + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/create-user-request.json" + } + } + } + } + } + }, + "/org/{shortname}/user/{username}": { + "get": { + "tags": [ + "Users" + ], + "summary": "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about a user in the same organization

Secretariat: Retrieves any user's information

", + "operationId": "userSingle", "parameters": [ { "name": "shortname", @@ -2652,6 +2590,15 @@ }, "description": "The shortname of the organization" }, + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The username of the user" + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2664,11 +2611,11 @@ ], "responses": { "200": { - "description": "Returns information about the organization updated", + "description": "Returns information about the specified user", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/update-registry-org-response.json" + "$ref": "../schemas/user/get-user-response.json" } } } @@ -2723,35 +2670,15 @@ } } } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "../schemas/registry-org/update-registry-org-request.json" - }, - "example": { - "short_name": "fake_company", - "long_name": "Fake Company", - "id_quota": 1000, - "authority": [ - "CNA" - ] - } - } - } } - } - }, - "/registry/org/{shortname}/user": { - "post": { + }, + "put": { "tags": [ - "Registry User" + "Users" ], - "summary": "Create a user with the provided short name as the owning organization (accessible to Secretariat or target organization Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Creates a user for the Admin's organization

Secretariat: Creates a user for any organization

", - "operationId": "registryUserCreateSingle", + "summary": "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)", + "description": "

Access Control

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

Expected Behavior

Regular User: Updates the user's own information. Only name fields may be changed.

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

", + "operationId": "userUpdateSingle", "parameters": [ { "name": "shortname", @@ -2762,6 +2689,42 @@ }, "description": "The shortname of the organization" }, + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The username of the user" + }, + { + "$ref": "#/components/parameters/active" + }, + { + "$ref": "#/components/parameters/activeUserRolesAdd" + }, + { + "$ref": "#/components/parameters/activeUserRolesRemove" + }, + { + "$ref": "#/components/parameters/nameFirst" + }, + { + "$ref": "#/components/parameters/nameLast" + }, + { + "$ref": "#/components/parameters/nameMiddle" + }, + { + "$ref": "#/components/parameters/nameSuffix" + }, + { + "$ref": "#/components/parameters/newUsername" + }, + { + "$ref": "#/components/parameters/orgShortname" + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2774,26 +2737,11 @@ ], "responses": { "200": { - "description": "Returns the new user information (with the secret)", + "description": "Returns the updated user information", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/create-registry-user-response.json" - } - }, - "example": { - "message": "jdoe was successfully created.", - "created": { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "status": "active", - "secret": "12345-abcde-67890", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" + "$ref": "../schemas/user/update-user-response.json" } } } @@ -2848,31 +2796,13 @@ } } } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "../schemas/registry-user/create-registry-user-request.json" - } - }, - "example": { - "username": "jdoe", - "status": "active", - "name": { - "first": "John", - "last": "Doe" - } - } - } } } }, - "/registry/org/{shortname}/user/{username}/reset_secret": { + "/org/{shortname}/user/{username}/reset_secret": { "put": { "tags": [ - "Registry User" + "Users" ], "summary": "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)", "description": "

Access Control

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

Expected Behavior

Regular User: Resets user's own API secret

Admin User: Resets any user's API secret in the Admin's organization

Secretariat: Resets any user's API secret

", @@ -2970,32 +2900,17 @@ } } }, - "/registry/org/{shortname}/user/{username}/grant-role": { - "post": { + "/users": { + "get": { "tags": [ - "Registry User" + "Users" ], - "summary": "Grants a role to a user (accessible to Secretariat or Org Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Grants a role to a user in the Admin's organization

Secretariat: Grants a role to a user in any organization

", - "operationId": "registryUserGrantRole", + "summary": "Retrieves information about all registered users (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all users for all organizations

", + "operationId": "userAll", "parameters": [ { - "name": "shortname", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The shortname of the organization" - }, - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The username of the user" + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3009,16 +2924,11 @@ ], "responses": { "200": { - "description": "Role granted successfully", + "description": "Returns all users, along with pagination fields if results span multiple pages of data.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } + "$ref": "../schemas/user/list-users-response.json" } } } @@ -3073,56 +2983,35 @@ } } } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "role": { - "type": "string", - "enum": [ - "ADMIN" - ] - } - }, - "required": [ - "role" - ] - } - } + } + } + }, + "/health-check": { + "get": { + "tags": [ + "Utilities" + ], + "summary": "Checks that the system is running (accessible to all users)", + "description": "

Access Control

Endpoint is accessible to all

Expected Behavior

Returns a 200 response code when CVE Services are running

", + "operationId": "healthCheck", + "responses": { + "200": { + "description": "Returns a 200 response code" } } } }, - "/registry/org/{shortname}/user/{username}/revoke-role": { - "post": { + "/registry/org": { + "get": { "tags": [ - "Registry User" + "Registry Organization" ], - "summary": "Revokes a role from a user (accessible to Secretariat or Org Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Revokes a role from a user in the Admin's organization

Secretariat: Revokes a role from a user in any organization

", - "operationId": "registryUserRevokeRole", + "summary": "Retrieves all registry organizations (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all registry organizations

", + "operationId": "registryOrgAll", "parameters": [ { - "name": "shortname", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The shortname of the organization" - }, - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The username of the user" + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3136,16 +3025,92 @@ ], "responses": { "200": { - "description": "Role revoked successfully", + "description": "Returns information about all registry organizations, along with pagination fields if results span multiple pages of data", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } + "$ref": "../schemas/registry-org/list-registry-orgs-response.json" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/bad-request.json" + } + } + } + }, + "401": { + "description": "Not Authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + }, + "post": { + "tags": [ + "Registry Organization" + ], + "summary": "Creates an organization (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates a new organization

", + "operationId": "registryOrgCreateSingle", + "parameters": [ + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-org/list-registry-orgs-response.json" } } } @@ -3206,32 +3171,367 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "role": { - "type": "string", - "enum": [ - "ADMIN" - ] + "anyOf": [ + { + "$ref": "../schemas/registry-org/SecretariatOrg.json" + }, + { + "$ref": "../schemas/registry-org/CNAOrg.json" + }, + { + "$ref": "../schemas/registry-org/ADPOrg.json" + }, + { + "$ref": "../schemas/registry-org/BulkDownloadOrg.json" } - }, - "required": [ - "role" ] + }, + "example": { + "short_name": "fake_company", + "long_name": "Fake Company", + "id_quota": 1000, + "authority": [ + "CNA" + ] + } + } + } + } + } + }, + "/registry/org/{shortname}/users": { + "get": { + "tags": [ + "Registry User" + ], + "summary": "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about users in the same organization

Secretariat: Retrieves all user information for any organization

", + "operationId": "registryOrgUsersAll", + "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the organization" + }, + { + "$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/registry-user/list-registry-users-response.json" + }, + "example": { + "totalCount": 1, + "itemsPerPage": 100, + "pageCount": 1, + "currentPage": 1, + "prevPage": null, + "nextPage": null, + "users": [ + { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "role": "ADMIN", + "status": "active", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" + } + ] + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/registry/org/{shortname}/id_quota": { + "get": { + "tags": [ + "Registry Organization" + ], + "summary": "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

Secretariat: Retrieves the CVE ID quota for any organization

", + "operationId": "registryOrgIdQuota", + "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the organization" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Returns the CVE ID quota for an organization", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-org/get-registry-org-quota-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" + } + } + } + } + } + } + }, + "/registry/org/{identifier}": { + "get": { + "tags": [ + "Registry Organization" + ], + "summary": "Retrieves information about the registry organization specified by short name or UUID (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization

Secretariat: Retrieves information about any registry organization

", + "operationId": "registryOrgSingle", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname or UUID of the registry organization" + }, + { + "name": "expand", + "in": "query", + "description": "Optional expanded related data. Accepted value: users.", + "required": false, + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "string" + }, + "enum": { + "type": "array", + "example": [ + "users" + ], + "items": { + "type": "string" + } + } + } + } + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Returns the registry organization information", + "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" + } + } + } + }, + "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" + } } } } } } }, - "/registry/org/{shortname}/conversation/{index}": { - "put": { + "/registry/org/{shortname}/user/{username}": { + "get": { "tags": [ - "Registry Organization" + "Registry User" ], - "summary": "Update the conversation at the given index for the given organization (accessible to Secretariat or original same-organization author)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be the original author of the conversation in the same organization

Expected Behavior

Original Author: Allowed to update only the message body of a conversation posted by them

Secretariat: Allowed to update the message body and/or visibility of any conversation

", - "operationId": "registryUserUpdateConversation", + "summary": "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about a registry user in the same organization

Secretariat: Retrieves any registry user's information

", + "operationId": "registryUserSingle", "parameters": [ { "name": "shortname", @@ -3239,17 +3539,15 @@ "required": true, "schema": { "type": "string" - }, - "description": "The shortname of the organization" + } }, { - "name": "index", + "name": "username", "in": "path", "required": true, "schema": { "type": "string" - }, - "description": "The index of the conversation to update" + } }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3263,11 +3561,11 @@ ], "responses": { "200": { - "description": "Returns the updated conversation", + "description": "Returns information about the specified registry user", "content": { "application/json": { "schema": { - "$ref": "../schemas/conversation/update-conversation-response.json" + "$ref": "../schemas/registry-user/get-registry-user-response.json" } } } @@ -3323,26 +3621,38 @@ } } } - } - }, - "/org": { - "get": { + }, + "put": { "tags": [ - "Organization" + "Registry User" ], - "summary": "Retrieves all organizations (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all organizations

", - "operationId": "orgAll", + "summary": "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)", + "description": "

Access Control

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

Expected Behavior

Regular User: Updates the user's own information. Only name fields may be changed.

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

", + "operationId": "registryUserUpdateSingle", "parameters": [ { - "name": "registry", - "in": "query", + "name": "shortname", + "in": "path", + "required": true, "schema": { "type": "string" - } + }, + "description": "The shortname of the organization" }, { - "$ref": "#/components/parameters/pageQuery" + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The username of the user" + }, + { + "$ref": "#/components/parameters/active" + }, + { + "$ref": "#/components/parameters/orgShortname" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3356,18 +3666,25 @@ ], "responses": { "200": { - "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data", + "description": "Returns the updated user information", "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/list-orgs-response.json" + "$ref": "../schemas/registry-user/update-registry-user-response.json" + }, + "example": { + "message": "jdoe was successfully updated.", + "updated": { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" }, - { - "$ref": "../schemas/registry-org/list-registry-orgs-response.json" - } - ] + "status": "active", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" + } } } } @@ -3424,14 +3741,32 @@ } } }, - "post": { + "delete": { "tags": [ - "Organization" + "Registry User" ], - "summary": "Creates an organization as specified in the request body (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates an organization

", - "operationId": "orgCreateSingle", + "summary": "Deletes the registry user specified by organization and username (accessible to Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Deletes the specified user from the specified organization

", + "operationId": "registryUserDeleteSingle", "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the organization" + }, + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The username of the user" + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3444,18 +3779,11 @@ ], "responses": { "200": { - "description": "Returns information about the organization created", + "description": "Confirms deletion of the registry user", "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/create-org-response.json" - }, - { - "$ref": "../schemas/registry-org/create-registry-org-response.json" - } - ] + "$ref": "../schemas/registry-user/delete-registry-user-response.json" } } } @@ -3510,36 +3838,26 @@ } } } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "../schemas/org/create-org-request.json" - } - } - } } } }, - "/org/{identifier}": { - "get": { + "/registry/org/{shortname}": { + "put": { "tags": [ - "Organization" + "Registry Organization" ], - "summary": "Retrieves information about the organization specified by short name or UUID (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves organization record for the specified shortname or UUID if it is the user's organization

Secretariat: Retrieves information about any organization

", - "operationId": "orgSingle", + "summary": "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

", + "operationId": "registryOrgUpdateSingle", "parameters": [ { - "name": "identifier", + "name": "shortname", "in": "path", "required": true, "schema": { "type": "string" }, - "description": "The shortname or UUID of the organization" + "description": "The shortname of the organization" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3553,11 +3871,11 @@ ], "responses": { "200": { - "description": "Returns the organization information", + "description": "Returns information about the organization updated", "content": { "application/json": { "schema": { - "$ref": "../schemas/org/get-org-response.json" + "$ref": "../schemas/registry-org/update-registry-org-response.json" } } } @@ -3612,17 +3930,33 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-org/update-registry-org-request.json" + }, + "example": { + "short_name": "fake_company", + "long_name": "Fake Company", + "id_quota": 1000, + "authority": [ + "CNA" + ] + } + } + } } - } - }, - "/org/{shortname}": { - "put": { + }, + "delete": { "tags": [ - "Organization" + "Registry Organization" ], - "summary": "Updates information about the organization specified by short name (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Updates any organization's information

", - "operationId": "orgUpdateSingle", + "summary": "Deletes the registry organization specified by short name (accessible to Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Deletes the specified registry organization

", + "operationId": "registryOrgDeleteSingle", "parameters": [ { "name": "shortname", @@ -3631,29 +3965,7 @@ "schema": { "type": "string" }, - "description": "The shortname of the organization" - }, - { - "name": "registry", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "$ref": "#/components/parameters/id_quota" - }, - { - "$ref": "#/components/parameters/name" - }, - { - "$ref": "#/components/parameters/newShortname" - }, - { - "$ref": "#/components/parameters/active_roles_add" - }, - { - "$ref": "#/components/parameters/active_roles_remove" + "description": "The shortname of the registry organization" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3667,11 +3979,11 @@ ], "responses": { "200": { - "description": "Returns information about the organization updated", + "description": "Confirms deletion of the registry organization", "content": { "application/json": { "schema": { - "$ref": "../schemas/org/update-org-response.json" + "$ref": "../schemas/registry-org/delete-registry-org-response.json" } } } @@ -3729,14 +4041,14 @@ } } }, - "/org/{shortname}/id_quota": { - "get": { + "/registry/org/{shortname}/user": { + "post": { "tags": [ - "Organization" + "Registry User" ], - "summary": "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

Secretariat: Retrieves the CVE ID quota for any organization

", - "operationId": "orgIdQuota", + "summary": "Create a user with the provided short name as the owning organization (accessible to Secretariat or target organization Admin)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Creates a user for the Admin's organization

Secretariat: Creates a user for any organization

", + "operationId": "registryUserCreateSingle", "parameters": [ { "name": "shortname", @@ -3759,11 +4071,26 @@ ], "responses": { "200": { - "description": "Returns the CVE ID quota for an organization", + "description": "Returns the new user information (with the secret)", "content": { "application/json": { "schema": { - "$ref": "../schemas/org/get-org-quota-response.json" + "$ref": "../schemas/registry-user/create-registry-user-response.json" + } + }, + "example": { + "message": "jdoe was successfully created.", + "created": { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "status": "active", + "secret": "12345-abcde-67890", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" } } } @@ -3818,17 +4145,35 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-user/create-registry-user-request.json" + } + }, + "example": { + "username": "jdoe", + "status": "active", + "name": { + "first": "John", + "last": "Doe" + } + } + } } } }, - "/org/{shortname}/users": { - "get": { + "/registry/org/{shortname}/user/{username}/reset_secret": { + "put": { "tags": [ - "Users" + "Registry User" ], - "summary": "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about users in the same organization

Secretariat: Retrieves all user information for any organization

", - "operationId": "userOrgAll", + "summary": "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)", + "description": "

Access Control

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

Expected Behavior

Regular User: Resets user's own API secret

Admin User: Resets any user's API secret in the Admin's organization

Secretariat: Resets any user's API secret

", + "operationId": "registryUserResetSecret", "parameters": [ { "name": "shortname", @@ -3840,14 +4185,13 @@ "description": "The shortname of the organization" }, { - "name": "registry", - "in": "query", + "name": "username", + "in": "path", + "required": true, "schema": { "type": "string" - } - }, - { - "$ref": "#/components/parameters/pageQuery" + }, + "description": "The username of the user" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3861,11 +4205,11 @@ ], "responses": { "200": { - "description": "Returns all users for the organization, along with pagination fields if results span multiple pages of data", + "description": "Returns the new API key", "content": { "application/json": { "schema": { - "$ref": "../schemas/user/list-users-response.json" + "$ref": "../schemas/user/reset-secret-response.json" } } } @@ -3923,14 +4267,14 @@ } } }, - "/org/{shortname}/user": { + "/registry/org/{shortname}/oversees/{shortname2}/add": { "post": { "tags": [ - "Users" + "Registry Organization" ], - "summary": "Create a user with the provided short name as the owning organization (accessible to Secretariat or target organization Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Creates a user for the Admin's organization

Secretariat: Creates a user for any organization

", - "operationId": "userCreateSingle", + "summary": "Assigns an organization to report to a ROOT organization (accessible to Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role.

Expected Behavior

The organization identified by shortname must have ROOT authority. The organization identified by shortname2 will report to it. If shortname2 already reported to a different ROOT. It will be removed from that and both will receive an audit entry.

", + "operationId": "registryOrgAddOverseeRelationship", "parameters": [ { "name": "shortname", @@ -3939,7 +4283,16 @@ "schema": { "type": "string" }, - "description": "The shortname of the organization" + "description": "The shortname of the overseeing ROOT organization" + }, + { + "name": "shortname2", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the reporting organization" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3953,11 +4306,16 @@ ], "responses": { "200": { - "description": "Returns the new user information (with the secret)", + "description": "Reports-to relationship added successfully", "content": { "application/json": { "schema": { - "$ref": "../schemas/user/create-user-response.json" + "type": "object", + "properties": { + "message": { + "type": "string" + } + } } } } @@ -4012,27 +4370,17 @@ } } } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "../schemas/user/create-user-request.json" - } - } - } } } }, - "/org/{shortname}/user/{username}": { - "get": { + "/registry/org/{shortname}/oversees/{shortname2}/remove": { + "post": { "tags": [ - "Users" + "Registry Organization" ], - "summary": "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about a user in the same organization

Secretariat: Retrieves any user's information

", - "operationId": "userSingle", + "summary": "Removes an organization reports-to relationship (accessible to Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role.

Expected Behavior

The organization identified by shortname must have ROOT authority. The organization identified by shortname2 will no longer report to it.

", + "operationId": "registryOrgRemoveOverseeRelationship", "parameters": [ { "name": "shortname", @@ -4041,16 +4389,16 @@ "schema": { "type": "string" }, - "description": "The shortname of the organization" + "description": "The shortname of the overseeing ROOT organization" }, { - "name": "username", + "name": "shortname2", "in": "path", "required": true, "schema": { "type": "string" }, - "description": "The username of the user" + "description": "The shortname of the reporting organization" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -4064,11 +4412,16 @@ ], "responses": { "200": { - "description": "Returns information about the specified user", + "description": "Reports-to relationship removed successfully", "content": { "application/json": { "schema": { - "$ref": "../schemas/user/get-user-response.json" + "type": "object", + "properties": { + "message": { + "type": "string" + } + } } } } @@ -4124,14 +4477,16 @@ } } } - }, - "put": { + } + }, + "/registry/org/{shortname}/user/{username}/grant-role": { + "post": { "tags": [ - "Users" + "Registry User" ], - "summary": "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)", - "description": "

Access Control

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

Expected Behavior

Regular User: Updates the user's own information. Only name fields may be changed.

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

", - "operationId": "userUpdateSingle", + "summary": "Grants a role to a user (accessible to Secretariat or Org Admin)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Grants a role to a user in the Admin's organization

Secretariat: Grants a role to a user in any organization

", + "operationId": "registryUserGrantRole", "parameters": [ { "name": "shortname", @@ -4151,33 +4506,6 @@ }, "description": "The username of the user" }, - { - "$ref": "#/components/parameters/active" - }, - { - "$ref": "#/components/parameters/activeUserRolesAdd" - }, - { - "$ref": "#/components/parameters/activeUserRolesRemove" - }, - { - "$ref": "#/components/parameters/nameFirst" - }, - { - "$ref": "#/components/parameters/nameLast" - }, - { - "$ref": "#/components/parameters/nameMiddle" - }, - { - "$ref": "#/components/parameters/nameSuffix" - }, - { - "$ref": "#/components/parameters/newUsername" - }, - { - "$ref": "#/components/parameters/orgShortname" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -4190,11 +4518,16 @@ ], "responses": { "200": { - "description": "Returns the updated user information", + "description": "Role granted successfully", "content": { "application/json": { "schema": { - "$ref": "../schemas/user/update-user-response.json" + "type": "object", + "properties": { + "message": { + "type": "string" + } + } } } } @@ -4249,17 +4582,38 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "ADMIN" + ] + } + }, + "required": [ + "role" + ] + } + } + } } } }, - "/org/{shortname}/user/{username}/reset_secret": { - "put": { + "/registry/org/{shortname}/user/{username}/revoke-role": { + "post": { "tags": [ - "Users" + "Registry User" ], - "summary": "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)", - "description": "

Access Control

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

Expected Behavior

Regular User: Resets user's own API secret

Admin User: Resets any user's API secret in the Admin's organization

Secretariat: Resets any user's API secret

", - "operationId": "userResetSecret", + "summary": "Revokes a role from a user (accessible to Secretariat or Org Admin)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Revokes a role from a user in the Admin's organization

Secretariat: Revokes a role from a user in any organization

", + "operationId": "registryUserRevokeRole", "parameters": [ { "name": "shortname", @@ -4291,11 +4645,16 @@ ], "responses": { "200": { - "description": "Returns the new API key", + "description": "Role revoked successfully", "content": { "application/json": { "schema": { - "$ref": "../schemas/user/reset-secret-response.json" + "type": "object", + "properties": { + "message": { + "type": "string" + } + } } } } @@ -4350,23 +4709,56 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "ADMIN" + ] + } + }, + "required": [ + "role" + ] + } + } + } } } }, - "/registry/users": { - "get": { + "/registry/org/{shortname}/conversation/{index}": { + "put": { "tags": [ - "Registry User" + "Registry Organization" ], - "summary": "Retrieves information about all registered users (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all users for all organizations

", - "operationId": "userAll", + "summary": "Update the conversation at the given index for the given organization (accessible to Secretariat or original same-organization author)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be the original author of the conversation in the same organization

Expected Behavior

Original Author: Allowed to update only the message body of a conversation posted by them

Secretariat: Allowed to update the message body and/or visibility of any conversation

", + "operationId": "registryOrgUpdateConversation", "parameters": [ { - "$ref": "#/components/parameters/pageQuery" + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the organization" }, { - "$ref": "#/components/parameters/registry" + "name": "index", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The index of the conversation to update" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -4380,11 +4772,11 @@ ], "responses": { "200": { - "description": "Returns all users, along with pagination fields if results span multiple pages of data.", + "description": "Returns the updated conversation", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/list-registry-users-response.json" + "$ref": "../schemas/conversation/update-conversation-response.json" } } } @@ -4442,21 +4834,18 @@ } } }, - "/users": { + "/registry/users": { "get": { "tags": [ - "Users" + "Registry User" ], "summary": "Retrieves information about all registered users (accessible to Secretariat)", "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all users for all organizations

", - "operationId": "userAll", + "operationId": "registryUserAll", "parameters": [ { "$ref": "#/components/parameters/pageQuery" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -4473,14 +4862,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/user/list-users-response.json" - }, - { - "$ref": "../schemas/registry-user/list-registry-users-response.json" - } - ] + "$ref": "../schemas/registry-user/list-registry-users-response.json" } } } @@ -4538,21 +4920,6 @@ } } }, - "/health-check": { - "get": { - "tags": [ - "Utilities" - ], - "summary": "Checks that the system is running (accessible to all users)", - "description": "

Access Control

Endpoint is accessible to all

Expected Behavior

Returns a 200 response code when CVE Services are running

", - "operationId": "healthCheck", - "responses": { - "200": { - "description": "Returns a 200 response code" - } - } - } - }, "/conversation": { "get": { "tags": [ @@ -6104,15 +6471,6 @@ "minimum": 1 } }, - "registry": { - "in": "query", - "name": "registry", - "description": "When set to true, the endpoint will expect request data to conform to the applicable User Registry schema, and will provide response data conforming to the applicable User Registry schema. Defaults to false.", - "required": false, - "schema": { - "type": "boolean" - } - }, "short_name": { "in": "query", "name": "short_name", diff --git a/docker/.docker-env.example b/docker/.docker-env.example index 277b5d01b..988311f94 100644 --- a/docker/.docker-env.example +++ b/docker/.docker-env.example @@ -1,4 +1,5 @@ LOCAL_KEY=TCF25YM-39C4H6D-KA32EGF-V5XSHN3 +MONGO_CONN_STRING=mongodb://docdb:27017,docdb-read-1:27017,docdb-read-2:27017/cve_dev?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false MONGO_HOST=docdb MONGO_PORT=27017 NODE_ENV=development diff --git a/docker/.docker-env.int-example b/docker/.docker-env.int-example index 924f472ab..c3a82240d 100644 --- a/docker/.docker-env.int-example +++ b/docker/.docker-env.int-example @@ -1,3 +1,4 @@ +MONGO_CONN_STRING=mongodb://docdb:27017,docdb-read-1:27017,docdb-read-2:27017/cve_int?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false MONGO_HOST=docdb MONGO_PORT=27017 NODE_ENV=integration diff --git a/docker/README.md b/docker/README.md index 37c4b66ab..37f875d88 100644 --- a/docker/README.md +++ b/docker/README.md @@ -177,6 +177,36 @@ See the [API documentation](https://github.com/CVEProject/cve-services#api-docum The `docker-compose.yml` file exposes the default Mongo port to the host: `localhost:27017`. You can connect using any Mongo viewer such as [Mongo Express](https://github.com/mongo-express/mongo-express) or [Compass](https://www.mongodb.com/try/download/compass) on the host. +### Run the Mongo Replica Cluster Only + +The Mongo-only compose file starts one primary and two replica members without starting the CVE Services app. By default it uses `mongo:5.0`. + +```bash +cd docker/ +docker compose -f docker-compose.mongo-cluster.yml up -d docdb docdb-read-1 docdb-read-2 mongo-init +``` + +Use this connection string from the host, including MongoDB Compass: + +```text +mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false +``` + +To run the same local replica topology with Mongo 8, override the image: + +```bash +cd docker/ +MONGO_IMAGE=mongo:8.0 docker compose -f docker-compose.mongo-cluster.yml up -d --force-recreate docdb docdb-read-1 docdb-read-2 mongo-init +``` + +If you already created the local volumes with Mongo 5, Mongo 8 may fail to start against those files. For a fresh Mongo 8 local cluster, remove the Mongo-only volumes first. This deletes local Mongo data for this compose file: + +```bash +cd docker/ +docker compose -f docker-compose.mongo-cluster.yml down -v +MONGO_IMAGE=mongo:8.0 docker compose -f docker-compose.mongo-cluster.yml up -d docdb docdb-read-1 docdb-read-2 mongo-init +``` + ## Running unit tests You can run unit tests using the docker image by running the following command: diff --git a/docker/docker-compose.mongo-cluster.yml b/docker/docker-compose.mongo-cluster.yml new file mode 100644 index 000000000..60e925b7e --- /dev/null +++ b/docker/docker-compose.mongo-cluster.yml @@ -0,0 +1,132 @@ +services: + docdb: + image: ${MONGO_IMAGE:-mongo:5.0} + container_name: mongo + ports: + - "27017:27017" + - "27018:27018" + - "27019:27019" + volumes: + - docdb-host-data:/data/db + command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--port", "27017"] + healthcheck: + test: ["CMD-SHELL", "mongosh --quiet --port 27017 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] + interval: 10s + timeout: 10s + retries: 12 + start_period: 30s + + docdb-read-1: + image: ${MONGO_IMAGE:-mongo:5.0} + container_name: mongo-read-1 + network_mode: "service:docdb" + depends_on: + docdb: + condition: service_healthy + volumes: + - docdb-host-read-1-data:/data/db + command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--port", "27018"] + healthcheck: + test: ["CMD-SHELL", "mongosh --quiet --port 27018 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] + interval: 10s + timeout: 10s + retries: 12 + start_period: 30s + + docdb-read-2: + image: ${MONGO_IMAGE:-mongo:5.0} + container_name: mongo-read-2 + network_mode: "service:docdb" + depends_on: + docdb: + condition: service_healthy + volumes: + - docdb-host-read-2-data:/data/db + command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--port", "27019"] + healthcheck: + test: ["CMD-SHELL", "mongosh --quiet --port 27019 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] + interval: 10s + timeout: 10s + retries: 12 + start_period: 30s + + mongo-init: + image: ${MONGO_IMAGE:-mongo:5.0} + network_mode: "service:docdb" + depends_on: + docdb: + condition: service_healthy + docdb-read-1: + condition: service_healthy + docdb-read-2: + condition: service_healthy + command: > + sh -c " + mongosh --host localhost --port 27017 --eval ' + const desiredConfig = { + _id: \"rs0\", + members: [ + { _id: 0, host: \"localhost:27017\", priority: 2 }, + { _id: 1, host: \"localhost:27018\", priority: 1 }, + { _id: 2, host: \"localhost:27019\", priority: 1 } + ] + }; + + function memberKey(member) { + return member._id + \":\" + member.host + \":\" + (member.priority ?? 1); + } + + function configMatches(currentConfig) { + const currentMembers = currentConfig.members.map(memberKey).sort().join(\"|\"); + const desiredMembers = desiredConfig.members.map(memberKey).sort().join(\"|\"); + return currentConfig._id === desiredConfig._id && currentMembers === desiredMembers; + } + + try { + rs.status(); + const currentConfig = rs.conf(); + if (configMatches(currentConfig)) { + print(\"Replica set already initialized with the expected localhost members.\"); + } else { + print(\"Updating replica set members...\"); + rs.reconfig({ ...desiredConfig, version: currentConfig.version + 1 }); + } + } catch (e) { + if (e.codeName == \"NotYetInitialized\") { + print(\"Initiating replica set...\"); + rs.initiate(desiredConfig); + } else { + throw e; + } + } + + for (let i = 0; i < 120; i++) { + let status; + try { + status = rs.status(); + } catch (e) { + print(\"Waiting for replica set status: \" + e.message); + sleep(2000); + continue; + } + + const primaryCount = status.members.filter(member => member.stateStr === \"PRIMARY\").length; + const secondaryCount = status.members.filter(member => member.stateStr === \"SECONDARY\").length; + + if (primaryCount === 1 && secondaryCount === 2) { + print(\"Replica set is ready with one primary and two secondaries.\"); + quit(0); + } + + print(\"Waiting for replica set readiness: primary=\" + primaryCount + \", secondary=\" + secondaryCount); + sleep(2000); + } + + throw new Error(\"Replica set did not become ready in time.\"); + ' + " + +volumes: + docdb-host-data: + docdb-host-read-1-data: + docdb-host-read-2-data: diff --git a/package-lock.json b/package-lock.json index 088d21fea..18cd66a37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.8.3", + "version": "2.8.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.8.3", + "version": "2.8.4", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", @@ -30,7 +30,7 @@ "mongo-cursor-pagination": "^8.1.3", "mongoose": "^8.9.5", "mongoose-aggregate-paginate-v2": "1.0.6", - "morgan": "^1.9.1", + "morgan": "^1.11.0", "node-dev": "^7.4.3", "packageurl-js": "^2.0.1", "prompt-sync": "^4.2.0", @@ -432,9 +432,9 @@ "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -443,9 +443,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -523,9 +523,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1477,9 +1477,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -1516,9 +1516,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -2926,9 +2926,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -3021,9 +3021,9 @@ } }, "node_modules/eslint-plugin-node/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -3169,9 +3169,9 @@ "license": "Python-2.0" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -3180,9 +3180,9 @@ } }, "node_modules/eslint/node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -3454,9 +3454,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "funding": [ { "type": "github", @@ -5099,9 +5099,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "license": "MIT", "dependencies": { @@ -5659,9 +5659,9 @@ "license": "Python-2.0" }, "node_modules/mocha/node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -5793,9 +5793,9 @@ } }, "node_modules/mongoose": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.0.tgz", - "integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.2.tgz", + "integrity": "sha512-5+H3MSHNJCcr9M+lVplekrZ4/Dyn3N1dOpvgn9gMwvHJhunc4G8SQcBVd/btBQoVdspVNPjx8Pw03YWBv6uTJg==", "license": "MIT", "dependencies": { "bson": "^6.10.4", @@ -5824,19 +5824,23 @@ } }, "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", - "on-finished": "~2.3.0", + "on-finished": "~2.4.1", "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/morgan/node_modules/debug": { @@ -5854,18 +5858,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -5914,9 +5906,9 @@ } }, "node_modules/multimatch/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -5938,9 +5930,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -6196,9 +6188,9 @@ } }, "node_modules/nyc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -7076,9 +7068,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -7096,7 +7088,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7561,9 +7553,9 @@ } }, "node_modules/replace-in-file/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7764,9 +7756,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -8451,9 +8443,9 @@ } }, "node_modules/standard/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -9151,9 +9143,9 @@ } }, "node_modules/swagger-autogen/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9250,9 +9242,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -10005,9 +9997,9 @@ } }, "node_modules/yamljs/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -10119,4 +10111,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 318a4bcec..00409c63f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cve-services", "author": "Automation Working Group", - "version": "2.8.3", + "version": "2.8.4", "license": "(CC0)", "devDependencies": { "@faker-js/faker": "^7.6.0", @@ -48,7 +48,7 @@ "mongo-cursor-pagination": "^8.1.3", "mongoose": "^8.9.5", "mongoose-aggregate-paginate-v2": "1.0.6", - "morgan": "^1.9.1", + "morgan": "^1.11.0", "node-dev": "^7.4.3", "packageurl-js": "^2.0.1", "prompt-sync": "^4.2.0", @@ -104,6 +104,7 @@ "swagger-autogen": "node src/swagger.js", "test": "NODE_ENV=test mocha --recursive --exit || true", "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING=mongodb://docdb:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test mocha test/integration-tests --recursive --exit", + "test:integration:replicas": "NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node src/scripts/runMocha.js test/integration-tests --recursive --exit", "test:unit-tests": "NODE_ENV=test mocha test/unit-tests --recursive --exit || true", "test:coverage": "NODE_ENV=test nyc --reporter=text mocha src/* --recursive --exit || true", "test:coverage-html": "NODE_ENV=test nyc --reporter=html mocha src/* --recursive --exit || true", diff --git a/schemas/registry-org/delete-registry-org-response.json b/schemas/registry-org/delete-registry-org-response.json new file mode 100644 index 000000000..f35e807f1 --- /dev/null +++ b/schemas/registry-org/delete-registry-org-response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cve.mitre.org/schema/registry-org/delete-registry-org-response.json", + "type": "object", + "title": "CVE Delete Registry Org Response", + "description": "JSON Schema for a successful registry organization deletion response", + "properties": { + "message": { + "type": "string", + "description": "Confirmation that the registry organization was deleted" + } + }, + "required": [ + "message" + ], + "additionalProperties": false +} diff --git a/schemas/registry-user/delete-registry-user-response.json b/schemas/registry-user/delete-registry-user-response.json new file mode 100644 index 000000000..caf60671d --- /dev/null +++ b/schemas/registry-user/delete-registry-user-response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cve.mitre.org/schema/registry-user/delete-registry-user-response.json", + "type": "object", + "title": "CVE Delete Registry User Response", + "description": "JSON Schema for a successful registry user deletion response", + "properties": { + "message": { + "type": "string", + "description": "Confirmation that the registry user was deleted" + } + }, + "required": [ + "message" + ], + "additionalProperties": false +} diff --git a/src/controller/cve-id.controller/cve-id.controller.js b/src/controller/cve-id.controller/cve-id.controller.js index ba93ea02f..e33b86411 100644 --- a/src/controller/cve-id.controller/cve-id.controller.js +++ b/src/controller/cve-id.controller/cve-id.controller.js @@ -39,8 +39,9 @@ async function getFilteredCveId (req, res, next) { const requesterOrgUUID = await authContext.getRequesterOrgUUID(req, orgRepo) // Create map of orgUUID to shortnames and users to simplify aggregation later - const orgs = await orgRepo.getAllOrgs() - const users = await userRepo.getAllUsers() + // Only project the fields needed for the maps to avoid fetching full documents + const orgs = await orgRepo.getAllOrgs({}, { UUID: 1, short_name: 1, _id: 0 }) + const users = await userRepo.getAllUsers({}, { UUID: 1, username: 1, org_UUID: 1, _id: 0 }) const orgMap = {} const userMap = {} diff --git a/src/controller/format.constants.js b/src/controller/format.constants.js new file mode 100644 index 000000000..37a215e0a --- /dev/null +++ b/src/controller/format.constants.js @@ -0,0 +1,7 @@ +const LEGACY_FORMAT = true +const REGISTRY_FORMAT = false + +module.exports = { + LEGACY_FORMAT, + REGISTRY_FORMAT +} diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 8e362cf2f..52e4c4c77 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -3,1201 +3,13 @@ const router = express.Router() const mw = require('../../middleware/middleware') const errorMsgs = require('../../middleware/errorMessages') const controller = require('./org.controller') -const registryOrgController = require('../registry-org.controller/registry-org.controller.js') -const registryUserController = require('../registry-user.controller/registry-user.controller.js') const { body, param, query } = require('express-validator') const { parseGetParams, parsePostParams, parsePutParams, parseError, isUserRole, isValidUsername, isOrgRole, validateUpdateOrgParameters, shortCircuitLegacyCpsMitreOrgParameters } = require('./org.middleware') // Only God and Javascript know swhy its saying it is not used when it is..... // eslint-disable-next-line no-unused-vars -const { toUpperCaseArray, isFlatStringArray, handleRegistryParameter } = require('../../middleware/middleware') +const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const getConstants = require('../../../src/constants').getConstants const CONSTANTS = getConstants() - -router.get('/registry/org', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'registryOrgAll' - #swagger.summary = "Retrieves all registry organizations (accessible to Secretariat)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role

-

Expected Behavior

-

Secretariat: Retrieves information about all registry organizations

" - #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns information about all registry organizations, along with pagination fields if results span multiple pages of data', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-org/list-registry-orgs-response.json' - } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - parseError, - parseGetParams, - registryOrgController.ALL_ORGS -) - -router.get('/registry/org/:shortname/users', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'userOrgAll' - #swagger.summary = "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

-

Expected Behavior

-

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/registry-user/list-registry-users-response.json' - }, - example: { - "totalCount": 1, - "itemsPerPage": 100, - "pageCount": 1, - "currentPage": 1, - "prevPage": null, - "nextPage": null, - "users": [ - { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "role": "ADMIN", - "status": "active", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } - ] - } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - parseError, - parseGetParams, - registryOrgController.USER_ALL) - -router.get('/registry/org/:shortname/id_quota', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'orgIdQuota' - #swagger.summary = "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

-

Expected Behavior

-

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

-

Secretariat: Retrieves the CVE ID quota for any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the CVE ID quota for an organization', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-org/get-registry-org-quota-response.json' - } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), - parseError, - parseGetParams, - controller.ORG_ID_QUOTA) - -router.get('/registry/org/:identifier', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'registryOrgSingle' - #swagger.summary = "Retrieves information about the registry organization specified by short name or UUID (accessible to same-organization users or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

-

Expected Behavior

-

Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization

-

Secretariat: Retrieves information about any registry organization

" - #swagger.parameters['identifier'] = { description: 'The shortname or UUID of the registry organization' } - #swagger.parameters['expand'] = { - in: 'query', - description: 'Optional expanded related data. Accepted value: users.', - required: false, - schema: { - type: 'string', - enum: ['users'] - } - } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the registry organization information', - 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[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['expand']) }), - query(['expand']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - query(['expand']).optional().isIn(['users']), - parseError, - parseGetParams, - registryOrgController.SINGLE_ORG -) - -router.get('/registry/org/:shortname/user/:username', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserSingle' - #swagger.summary = "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

-

Expected Behavior

-

Regular, CNA & Admin Users: Retrieves information about a registry user in the same organization

-

Secretariat: Retrieves any registry user's information

" - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.parameters['shortname'] = { - description: 'The shortname of the organization' - } - #swagger.parameters['username'] = { - description: 'The username of the registry user', - schema: { - type: 'string', - pattern: '^[a-zA-Z0-9._@-]+$' - } - } - #swagger.responses[200] = { - description: 'Returns information about the specified registry user', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/get-registry-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.useRegistry(), - mw.validateUser, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - param(['username']).isString().trim().notEmpty().custom(isValidUsername), - query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), - parseError, - parseGetParams, - registryUserController.SINGLE_USER -) - -router.post('/registry/org', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'orgCreateSingle' - #swagger.summary = "Creates an organization (accessible to Secretariat)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role

-

Expected Behavior

-

Secretariat: Creates a new organization

" - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { - anyOf: [ - { $ref: '../schemas/registry-org/SecretariatOrg.json' }, - { $ref: '../schemas/registry-org/CNAOrg.json' }, - { $ref: '../schemas/registry-org/ADPOrg.json' }, - { $ref: '../schemas/registry-org/BulkDownloadOrg.json' } - ] - }, - example: { - short_name: 'fake_company', - long_name: 'Fake Company', - id_quota: 1000, - authority: ['CNA'] - } - } - } - } - #swagger.responses[200] = { - description: 'Returns information about all organizations, along with pagination fields if results span multiple pages of data', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-org/list-registry-orgs-response.json' - } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), - parsePostParams, - parseError, - registryOrgController.CREATE_ORG -) - -router.put('/registry/org/:shortname', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'orgUpdateSingle' - #swagger.summary = "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

-

With Joint Approval required for the following fields:

-

Expected Behavior

- This endpoint expects a full organization object in the request body. -

Secretariat: Updates any organization's information

-

Organization Admin: Requests changes to its organization's information

- " - #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/registry-org/update-registry-org-request.json' - }, - example: { - short_name: 'fake_company', - long_name: 'Fake Company', - id_quota: 1000, - authority: ['CNA'] - } - } - } - } - #swagger.responses[200] = { - description: 'Returns information about the organization updated', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-org/update-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[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - // mw.onlySecretariat, - parseError, - parsePutParams, - registryOrgController.UPDATE_ORG -) - -router.post('/registry/org/: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 Secretariat or target organization Admin)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be an Admin of the target organization

-

Expected Behavior

-

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/registry-user/create-registry-user-request.json' } - }, - example: { - "username": "jdoe", - "status": "active", - "name": { - "first": "John", - "last": "Doe" - } - } - } - } - #swagger.responses[200] = { - description: 'Returns the new user information (with the secret)', - content: { - "application/json": { - schema: - { $ref: '../schemas/registry-user/create-registry-user-response.json' } - }, - example: { - "message": "jdoe was successfully created.", - "created": { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "status": "active", - "secret": "12345-abcde-67890", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlySecretariatOrAdmin, - mw.onlyOrgWithPartnerRole, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - body(['org_uuid']).optional().isString().trim(), - body(['uuid']).optional().isString().trim(), - body(['name.first']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_FIRSTNAME_LENGTH }).withMessage(errorMsgs.FIRSTNAME_LENGTH), - body(['name.last']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_LASTNAME_LENGTH }).withMessage(errorMsgs.LASTNAME_LENGTH), - body(['name.middle']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_MIDDLENAME_LENGTH }).withMessage(errorMsgs.MIDDLENAME_LENGTH), - body(['name.suffix']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_SUFFIX_LENGTH }).withMessage(errorMsgs.SUFFIX_LENGTH), - body(['authority.active_roles']).optional() - .custom(mw.isFlatStringArray) - .bail() - .customSanitizer(toUpperCaseArray) - .custom(isUserRole), - parseError, - parsePostParams, - registryOrgController.USER_CREATE_SINGLE -) - -router.put('/registry/org/:shortname/user/:username', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserUpdateSingle' - #swagger.summary = "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

-

Expected Behavior

-

Regular User: Updates the user's own information. Only name fields may be changed.

-

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

-

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['username'] = { description: 'The username of the user' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/active', - '#/components/parameters/orgShortname', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the updated user information', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/update-registry-user-response.json' }, - example: { - "message": "jdoe was successfully updated.", - "updated": { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "status": "active", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } - } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlyOrgWithPartnerRole, - parseError, - parsePutParams, - registryUserController.UPDATE_USER) - -router.put('/registry/org/:shortname/user/:username/reset_secret', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'userResetSecret' - #swagger.summary = "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

-

Expected Behavior

-

Regular User: Resets user's own API secret

-

Admin User: Resets any user's API secret in the Admin's organization

-

Secretariat: Resets any user's API secret

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['username'] = { description: 'The username of the user' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the new API key', - content: { - "application/json": { - schema: { $ref: '../schemas/user/reset-secret-response.json' } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlyOrgWithPartnerRole, - parseError, - parsePostParams, - controller.USER_RESET_SECRET -) - -router.post('/registry/org/:shortname/user/:username/grant-role', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserGrantRole' - #swagger.summary = "Grants a role to a user (accessible to Secretariat or Org Admin)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be an Admin of the target organization

-

Expected Behavior

-

Admin User: Grants a role to a user in the Admin's organization

-

Secretariat: Grants a role to a user in any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['username'] = { description: 'The username of the user' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - role: { - type: 'string', - enum: ['ADMIN'] - } - }, - required: ['role'] - } - } - } - } - #swagger.responses[200] = { - description: 'Role granted successfully', - content: { - "application/json": { - schema: { type: 'object', properties: { message: { type: 'string' } } } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - // mw.onlyOrgWithPartnerRole, // This might be too restrictive if we want Secretariat to do it for any org type - parseError, - parsePostParams, - registryUserController.GRANT_ROLE -) - -router.post('/registry/org/:shortname/user/:username/revoke-role', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserRevokeRole' - #swagger.summary = "Revokes a role from a user (accessible to Secretariat or Org Admin)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be an Admin of the target organization

-

Expected Behavior

-

Admin User: Revokes a role from a user in the Admin's organization

-

Secretariat: Revokes a role from a user in any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['username'] = { description: 'The username of the user' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - role: { - type: 'string', - enum: ['ADMIN'] - } - }, - required: ['role'] - } - } - } - } - #swagger.responses[200] = { - description: 'Role revoked successfully', - content: { - "application/json": { - schema: { type: 'object', properties: { message: { type: 'string' } } } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - // mw.onlyOrgWithPartnerRole, - parseError, - parsePostParams, - registryUserController.REVOKE_ROLE -) - -router.put('/registry/org/:shortname/conversation/:index', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'registryUserUpdateConversation' - #swagger.summary = "Update the conversation at the given index for the given organization (accessible to Secretariat or original same-organization author)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be the original author of the conversation in the same organization

-

Expected Behavior

-

Original Author: Allowed to update only the message body of a conversation posted by them

-

Secretariat: Allowed to update the message body and/or visibility of any conversation

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['index'] = { description: 'The index of the conversation to update' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the updated conversation', - content: { - "application/json": { - schema: { $ref: '../schemas/conversation/update-conversation-response.json' } - } - } - } - #swagger.responses[400] = { - description: 'Bad Request', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/bad-request.json' } - } - } - } - #swagger.responses[401] = { - description: 'Not Authenticated', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[403] = { - description: 'Forbidden', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlyOrgWithPartnerRole, - parseError, - parsePostParams, - registryOrgController.EDIT_CONVERSATION -) - router.get('/org', /* #swagger.tags = ['Organization'] @@ -1268,7 +80,6 @@ router.get('/org', } } */ - mw.handleRegistryParameter, mw.validateUser, mw.onlySecretariat, query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), @@ -1670,7 +481,6 @@ router.get('/org/:shortname/users', } } */ - mw.handleRegistryParameter, mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index cdadb284e..19fcd0bac 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -7,6 +7,11 @@ const error = new errors.OrgControllerError() const validateUUID = require('uuid').validate const _ = require('lodash') const authContext = require('../../utils/authContext') +const { LEGACY_FORMAT, REGISTRY_FORMAT } = require('../format.constants') + +function getObjectFormatForRequest (req) { + return req.useRegistry ? REGISTRY_FORMAT : LEGACY_FORMAT +} /** * Get the details of all orgs. @@ -32,7 +37,7 @@ async function getOrgs (req, res, next) { 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 returnValue = await repo.getAllOrgs({ ...options }, true) + const returnValue = await repo.getAllOrgs({ ...options }, LEGACY_FORMAT) logger.info({ uuid: req.ctx.uuid, message: 'The orgs were sent to the user.' }) return res.status(200).json(returnValue) @@ -58,21 +63,20 @@ async function getOrg (req, res, next) { const requesterOrgShortName = req.ctx.org const identifier = req.ctx.params.identifier const identifierIsUUID = validateUUID(identifier) - const returnLegacyFormat = true let returnValue try { - const requesterOrg = await authContext.getRequesterOrg(req, repo, {}, returnLegacyFormat) + const requesterOrg = await authContext.getRequesterOrg(req, repo, {}, LEGACY_FORMAT) // Ensure requester org exists if (!requesterOrg) { return res.status(404).json(error.orgDne(requesterOrgShortName, 'requesterOrgShortName', 'header')) } - const isSecretariat = await authContext.isRequesterSecretariat(req, repo, {}, returnLegacyFormat) + const isSecretariat = await authContext.isRequesterSecretariat(req, repo, {}, LEGACY_FORMAT) const isRequesterSameOrg = identifierIsUUID ? requesterOrg.UUID === identifier - : await authContext.isRequesterSameOrg(req, repo, identifier, {}, returnLegacyFormat) + : await authContext.isRequesterSameOrg(req, repo, identifier, {}, LEGACY_FORMAT) // Ensure that if the requester is not Secretariat, they can't view orgs other than their own if (!isRequesterSameOrg && !isSecretariat) { @@ -80,7 +84,7 @@ async function getOrg (req, res, next) { return res.status(403).json(error.notSameOrgOrSecretariat()) } - returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, returnLegacyFormat) + returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, LEGACY_FORMAT) } catch (err) { // Handle the specific error thrown by BaseOrgRepository.getOrg if (err.message && err.message.includes('Unknown Org type requested')) { @@ -139,13 +143,13 @@ async function getUsers (req, res, next) { return res.status(404).json(error.orgDnePathParam(orgShortName)) } - const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: orgUUID, short_name: orgShortName }, {}, !req.useRegistry) + const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: orgUUID, short_name: orgShortName }, {}, LEGACY_FORMAT) if (!isSameOrg && !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 payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, !!req.useRegistry) + const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, LEGACY_FORMAT) logger.info({ uuid: req.ctx.uuid, message: `The users of ${orgShortName} organization were sent to the user.` }) return res.status(200).json(payload) @@ -169,10 +173,10 @@ async function getUser (req, res, next) { const orgShortName = req.ctx.params.shortname const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, !req.useRegistry) + const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, LEGACY_FORMAT) const orgUUID = await orgRepo.getOrgUUID(orgShortName) - const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: orgUUID, short_name: orgShortName }, {}, !req.useRegistry) + const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: orgUUID, short_name: orgShortName }, {}, LEGACY_FORMAT) if (!isSameOrg && !isSecretariat) { logger.info({ uuid: req.ctx.uuid, message: req.ctx.org + ' organization can only be viewed by that organization\'s users or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) @@ -185,7 +189,7 @@ async function getUser (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() // This is simple, we can just call our function - const result = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName, {}, !!req.useRegistry) + const result = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName, {}, LEGACY_FORMAT) if (!result) { logger.info({ uuid: req.ctx.uuid, message: username + ' does not exist.' }) @@ -218,22 +222,22 @@ async function getOrgIdQuota (req, res, next) { try { const orgRepo = req.ctx.repositories.getBaseOrgRepository() const shortName = req.ctx.params.shortname - - const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, !req.useRegistry) - const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, shortName, {}, !req.useRegistry) + const objectFormat = getObjectFormatForRequest(req) + const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, objectFormat) + const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, shortName, {}, objectFormat) if (!isSameOrg && !isSecretariat) { logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization id quota can only be viewed by the users of the same organization or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) } - const org = await orgRepo.getOrg(shortName, false, {}, !req.useRegistry) + const org = await orgRepo.getOrg(shortName, false, {}, objectFormat) if (!org) { // a null org can only happen if the requestor is the Secretariat logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization does not exist.' }) return res.status(404).json(error.orgDnePathParam(shortName)) } - const returnPayload = await orgRepo.getOrgIdQuota(org, !req.useRegistry) + const returnPayload = await orgRepo.getOrgIdQuota(org, objectFormat) 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) } catch (err) { @@ -264,21 +268,8 @@ async function createOrg (req, res, next) { try { session.startTransaction({ readPreference: 'primary' }) - if (req.useRegistry) { - // If we are creating an org via the registry flag, we can do a full validation. - const result = await repo.validateOrg(body, { session }) - if (!result.isValid) { - logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) - await session.abortTransaction() - if (!Array.isArray(body?.authority) || body?.authority.some(item => typeof item !== 'string')) { - return res.status(400).json({ error: 'BAD_INPUT', message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) - } - return res.status(400).json({ error: 'BAD_INPUT', message: 'Parameters were invalid', errors: result.errors }) - } - } - // Check to see if the org already exits - if (await repo.orgExists(body?.short_name, { session }, !req.useRegistry)) { + if (await repo.orgExists(body?.short_name, { session }, LEGACY_FORMAT)) { logger.info({ uuid: req.ctx.uuid, message: body?.short_name + ' organization was not created because it already exists.' }) await session.abortTransaction() return res.status(400).json(error.orgExists(body?.short_name)) @@ -295,9 +286,9 @@ async function createOrg (req, res, next) { return res.status(400).json(error.aliasCollision(collisionString)) } const userRepo = req.ctx.repositories.getBaseUserRepository() - const isSecretariat = await authContext.isRequesterSecretariat(req, repo, { session }, !req.useRegistry) - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, repo, { session }, !!req.useRegistry) - returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, !req.useRegistry, requestingUserUUID, isSecretariat) + const isSecretariat = await authContext.isRequesterSecretariat(req, repo, { session }, LEGACY_FORMAT) + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, repo, { session }, LEGACY_FORMAT) + returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, LEGACY_FORMAT, requestingUserUUID, isSecretariat) await session.commitTransaction() } catch (error) { @@ -351,23 +342,6 @@ async function updateOrg (req, res, next) { try { session.startTransaction({ readPreference: 'primary' }) - // TODO: Check to see if this check is needed for both options - if (req.useRegistry) { - if (queryParametersJson['active_roles.add']) { - if (!Array.isArray(queryParametersJson.active_roles?.add) || queryParametersJson.active_roles?.add.some(item => typeof item !== 'string')) { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) - } - } - - if (queryParametersJson['active_roles.remove']) { - if (!Array.isArray(queryParametersJson.active_roles?.remove) || queryParametersJson.active_roles?.remove.some(item => typeof item !== 'string')) { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) - } - } - } - if (!(await orgRepository.orgExists(shortNameUrlParameter, { session }))) { logger.info({ uuid: req.ctx.uuid, message: `Organization ${shortNameUrlParameter} not found.` }) await session.abortTransaction() @@ -392,9 +366,9 @@ async function updateOrg (req, res, next) { } const userRepo = req.ctx.repositories.getBaseUserRepository() - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepository, { session }, !!req.useRegistry) - const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepository, { session }, !req.useRegistry) - const isAdmin = await authContext.isRequesterAdmin(req, userRepo, orgRepository, { session }, !!req.useRegistry) + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepository, { session }, LEGACY_FORMAT) + const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepository, { session }, LEGACY_FORMAT) + const isAdmin = await authContext.isRequesterAdmin(req, userRepo, orgRepository, { session }, LEGACY_FORMAT) if (!isSecretariat) { const secretariatOnlyFields = getConstants().SECRETARIAT_ONLY_FIELDS @@ -405,7 +379,7 @@ async function updateOrg (req, res, next) { return res.status(403).json(error.secretariatOnlyEditing(restrictedFieldsSent)) } } - const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, !req.useRegistry, requestingUserUUID, isAdmin, isSecretariat) + const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, LEGACY_FORMAT, requestingUserUUID, isAdmin, isSecretariat) responseMessage = { message: `${updatedOrg.short_name} organization was successfully updated.`, updated: updatedOrg } // Clarify message payload = { @@ -433,7 +407,7 @@ async function updateOrg (req, res, next) { /** * Creates a user only if the org exists and the user does not exist for the specified shortname and username. - * Called by POST /api/registry/org/{shortname}/user, POST /api/org/{shortname}/user + * Called by POST /api/org/{shortname}/user * * @param {Object} req - The request object * @param {Object} res - The response object @@ -446,7 +420,6 @@ async function createUser (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() const orgShortName = req.ctx.params.shortname - const constants = getConstants() let returnValue // Check to make sure Org Exists first @@ -469,30 +442,13 @@ async function createUser (req, res, next) { try { session.startTransaction({ readPreference: 'primary' }) - if (req.useRegistry) { - const result = await userRepo.validateUser(body) - if (body?.role && typeof body?.role !== 'string') { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) - } - if (body?.role && !constants.USER_ROLES.includes(body?.role)) { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: `Role must be one of the following: ${constants.USER_ROLES}` }] }) - } - if (!result.isValid) { - logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) - } - } else { - if (!body?.username || typeof body?.username !== 'string') { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'username', msg: 'Parameter must be a non empty string' }] }) - } + if (!body?.username || typeof body?.username !== 'string') { + await session.abortTransaction() + return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'username', msg: 'Parameter must be a non empty string' }] }) } // Ask repo if user already exists - if (await userRepo.orgHasUser(orgShortName, body?.username, { session }, !!req.useRegistry)) { + if (await userRepo.orgHasUser(orgShortName, body?.username, { session }, LEGACY_FORMAT)) { logger.info({ uuid: req.ctx.uuid, message: `${body?.username} user was not created because it already exists.` }) await session.abortTransaction() return res.status(400).json(error.userExists(body?.username)) @@ -500,10 +456,10 @@ async function createUser (req, res, next) { let isRequesterAdminOrSecretariat if (!req.ctx.authenticated && !req.ctx.orgUUID && typeof userRepo.isAdminOrSecretariat === 'function') { - isRequesterAdminOrSecretariat = await userRepo.isAdminOrSecretariat(orgShortName, req.ctx.user, req.ctx.org, { session }, !!req.useRegistry) + isRequesterAdminOrSecretariat = await userRepo.isAdminOrSecretariat(orgShortName, req.ctx.user, req.ctx.org, { session }, LEGACY_FORMAT) } else { - const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, !req.useRegistry) - const isRequesterAdminOfTargetOrg = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, orgShortName, { session }, !!req.useRegistry) + const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, LEGACY_FORMAT) + const isRequesterAdminOfTargetOrg = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, orgShortName, { session }, LEGACY_FORMAT) isRequesterAdminOrSecretariat = isRequesterSecretariat || isRequesterAdminOfTargetOrg } @@ -518,8 +474,8 @@ async function createUser (req, res, next) { return res.status(400).json(error.userLimitReached()) } - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, !!req.useRegistry) - returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, !!req.useRegistry, requestingUserUUID) + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, LEGACY_FORMAT) + returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, LEGACY_FORMAT, requestingUserUUID) await session.commitTransaction() } catch (error) { await session.abortTransaction() @@ -552,7 +508,7 @@ async function createUser (req, res, next) { /** * Updates a user only if the user exist for the specified username. * If no user exists, it does not create the user. - * Called by PUT /org/{shortname}/user/{username}, PUT /org/{shortname}/user/{username} + * Called by PUT /api/org/{shortname}/user/{username} * * @param {Object} req - The request object * @param {Object} res - The response object @@ -575,26 +531,20 @@ async function updateUser (req, res, next) { const queryParametersJson = req.ctx.query // Get requester UUID for later - const requesterUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, !!req.useRegistry) - const targetUserUUID = await userRepo.getUserUUID(usernameParams, shortNameParams, { session }, !!req.useRegistry) + const requesterUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, LEGACY_FORMAT) + const targetUserUUID = await userRepo.getUserUUID(usernameParams, shortNameParams, { session }, LEGACY_FORMAT) - const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, !req.useRegistry) - const isAdmin = await authContext.isRequesterAdmin(req, userRepo, orgRepo, { session }, !!req.useRegistry) + const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, LEGACY_FORMAT) + const isAdmin = await authContext.isRequesterAdmin(req, userRepo, orgRepo, { session }, LEGACY_FORMAT) const targetOrgUUID = await orgRepo.getOrgUUID(shortNameParams, { session }) - // if (req.useRegistry) { - // if (body?.role && typeof body?.role !== 'string') { - // return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) - // } - // } - if (!targetOrgUUID) { logger.info({ uuid: req.ctx.uuid, message: `Target organization ${shortNameParams} does not exist.` }) await session.abortTransaction() return res.status(404).json(error.orgDnePathParam(shortNameParams)) } - const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: targetOrgUUID, short_name: shortNameParams }, { session }, !req.useRegistry) + const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: targetOrgUUID, short_name: shortNameParams }, { session }, LEGACY_FORMAT) if (!requesterSameOrg && !isRequesterSecretariat) { logger.info({ uuid: req.ctx.uuid, message: `${shortNameParams} organization data can only be modified by users of the same organization or the Secretariat.` }) await session.abortTransaction() @@ -694,7 +644,7 @@ async function updateUser (req, res, next) { } } - const payload = await userRepo.updateUser(usernameParams, shortNameParams, queryParametersJson, { session }, !!req.useRegistry, requesterUUID) + const payload = await userRepo.updateUser(usernameParams, shortNameParams, queryParametersJson, { session }, LEGACY_FORMAT, requesterUUID) await session.commitTransaction() return res.status(200).json({ message: `${usernameParams} was successfully updated.`, updated: payload }) } catch (err) { @@ -715,7 +665,7 @@ async function updateUser (req, res, next) { /** * Resets API secret for specified user. - * Called by PUT /org/{shortname}/user/{username}/reset_secret, PUT /registry/org/{shortname}/user/{username}/reset_secret + * Called by PUT /api/org/{shortname}/user/{username}/reset_secret, PUT /api/registry/org/{shortname}/user/{username}/reset_secret * * @param {Object} req - The request object * @param {Object} res - The response object @@ -730,12 +680,13 @@ async function resetSecret (req, res, next) { const orgRepo = req.ctx.repositories.getBaseOrgRepository() const userRepo = req.ctx.repositories.getBaseUserRepository() + const objectFormat = getObjectFormatForRequest(req) try { session.startTransaction({ readPreference: 'primary' }) // Check if target org exists - const targetOrgUUID = await orgRepo.getOrgUUID(targetOrgShortName, { session }, !req.useRegistry) + const targetOrgUUID = await orgRepo.getOrgUUID(targetOrgShortName, { session }, objectFormat) if (!targetOrgUUID) { logger.info({ uuid: req.ctx.uuid, message: 'Org DNE' }) await session.abortTransaction() @@ -743,11 +694,11 @@ async function resetSecret (req, res, next) { } const targetOrg = { UUID: targetOrgUUID, short_name: targetOrgShortName } - const requesterUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, !!req.useRegistry) - const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, !req.useRegistry) + const requesterUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, objectFormat) + const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, objectFormat) if (!isRequesterSecretariat) { - const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, targetOrg, { session }, !req.useRegistry) + const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, targetOrg, { session }, objectFormat) if (!requesterSameOrg) { 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.' }) await session.abortTransaction() @@ -756,7 +707,7 @@ async function resetSecret (req, res, next) { } // Check if target user exists in target org - const targetUserUUID = await userRepo.getUserUUID(targetUsername, targetOrgShortName, { session }, !!req.useRegistry) + const targetUserUUID = await userRepo.getUserUUID(targetUsername, targetOrgShortName, { session }, objectFormat) if (!targetUserUUID) { logger.info({ uuid: req.ctx.uuid, message: 'User DNE' }) await session.abortTransaction() @@ -768,7 +719,7 @@ async function resetSecret (req, res, next) { // 1. WE are not the same user if (requesterUserUUID !== targetUserUUID) { // Check to see if we are the admin of the target organization - const isAdminOfTargetOrg = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, targetOrg, { session }, !!req.useRegistry) + const isAdminOfTargetOrg = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, targetOrg, { session }, objectFormat) if (!isAdminOfTargetOrg) { 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.' }) @@ -778,7 +729,7 @@ async function resetSecret (req, res, next) { } } - const updatedSecret = await userRepo.resetSecret(targetUsername, targetOrgShortName, { session }, !!req.useRegistry) + const updatedSecret = await userRepo.resetSecret(targetUsername, targetOrgShortName, { session }, objectFormat) logger.info({ uuid: req.ctx.uuid, message: `The API secret was successfully reset and sent to ${targetUsername}` }) const payload = { diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index d8331ecc9..fdff50735 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -2,7 +2,7 @@ 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 { param, query } = require('express-validator') const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const CONSTANTS = getConstants() const errorMsgs = require('../../middleware/errorMessages') @@ -22,186 +22,8 @@ function isOrgRole (val) { return true } -function validateCreateOrgParameters () { - return async (req, res, next) => { - const useRegistry = req.query.registry === 'true' - let validations = [] - if (useRegistry) { - // Not allowed - // users, , in_use, created, last_updated - const orgOptions = ['CNA', 'Secretariat', '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(['top_level_root']).default('') - .isString(), - body(['advisory_locations']) - .default([]) - .custom(isFlatStringArray), - body(['advisory_location_require_credentials']) - .default(false) - .isBoolean(), - body(['vulnerability_advisory_location_for_web_scraping']) - .default([]) - .custom(isFlatStringArray), - body(['tl_root_start_date']) - .default(null) - .isDate(), - body(['is_cna_discussion_list']) - .default(false) - .isBoolean(), - body([ - 'program_data.cve_website_update_date', - 'program_data.partner_active_date', - 'program_data.partner_inactive_date' - ]) - .optional({ nullable: true }) - .isDate(), - body(['program_data.cve_website_update_needed']) - .optional() - .isBoolean(), - body( - [ - 'charter_or_scope', - 'disclosure_policy', - 'product_list', - 'contact_info.websites', - 'contact_info.emails', - 'contact_info.phone', - '', - '', - 'partner_role_type', - 'partner_number', - 'partner_country', - 'program_data.status', - 'industry' - ]) - .default('') - .isString(), - body(['authority.active_roles']) - .default([CONSTANTS.AUTH_ROLE_ENUM.CNA]) - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole), - body(['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('reports_to', 'name', 'users', '', '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', - 'in_use', - 'created', - 'top_level_root', - 'aliases', - 'id_quota', - 'contact_info.phone', - 'contact_info.websites', - 'contact_info.emails', - 'contact_info', - 'users', - 'charter_or_scope', - 'disclosure_policy', - 'product_list', - 'contact_info.websites', - 'contact_info.emails', - 'contact_info.phone', - 'private_contacts', - 'partner_role_type', - 'partner_number', - 'partner_country', - 'program_data.cve_website_update_date', - 'program_data.cve_website_update_needed', - 'program_data.status', - 'advisory_locations', - 'advisory_location_require_credentials', - 'vulnerability_advisory_location_for_web_scraping', - 'industry', - 'tl_root_start_date', - 'is_cna_discussion_list') - ] - } - - const results = [] - for (const validation of validations) { - const result = await validation.run(req) - if (!result.isEmpty()) { - results.push(...result.errors) - } - } - if (results.length > 0) { - return res.status(400).json({ message: 'Parameters were invalid', details: results }) - } - next() - } -} - -function validateUserIdOrUsername () { - return async (req, res, next) => { - const useRegistry = req.query.registry === 'true' - const validations = [] - if (useRegistry) { - validations.push( - body('user_id') // Condition to run validation - .isString() - .trim() - .notEmpty(isValidUsername)) - } else { - validations.push(body('username').isString().trim().notEmpty(isValidUsername)) - } - const results = [] - for (const validation of validations) { - const result = await validation.run(req) - if (!result.isEmpty()) { - results.push(...result.errors) - } - } - if (results.length > 0) { - return res.status(400).json({ message: 'Parameters were invalid', details: results }) - } - next() - } -} - function validateUpdateOrgParameters () { return async (req, res, next) => { - const useRegistry = req.query.registry === 'true' const allowedParams = [...QUERY_PARAMETERS.shared] const registryParametersOnly = [...QUERY_PARAMETERS.registryOnly] @@ -218,43 +40,8 @@ function validateUpdateOrgParameters () { .custom(isFlatStringArray) .customSanitizer(toUpperCaseArray), // Path parameter validation - param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH })] - if (useRegistry) { - validations.push( - query(['oversees']).optional().isArray(), - query(['top_level_root']).optional().isString(), - query([ - 'charter_or_scope', - 'disclosure_policy', - 'product_list', - 'contact_info.websites', - 'contact_info.emails', - 'contact_info.phone', - '', - '', - 'partner_role_type', - 'partner_number', - 'partner_country', - 'program_data.cve_website_update_date', - 'program_data.cve_website_update_needed', - 'program_data.status', - 'advisory_location_require_credentials', - 'vulnerability_advisory_location_for_web_scraping', - 'advisory_locations', - 'industry', - 'tl_root_start_date', - 'is_cna_discussion_list' - ]) - .optional() - .isString() - .trim() - ) - } else { - validations.push( - // Block registry-only parameters - ...isNotAllowedQuery(...registryParametersOnly) - ) - } + param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + ...isNotAllowedQuery(...registryParametersOnly)] const results = [] for (const validation of validations) { @@ -270,16 +57,6 @@ function validateUpdateOrgParameters () { } } -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) @@ -391,6 +168,11 @@ function parseGetParams (req, res, next) { next() } +function parseDeleteParams (req, res, next) { + utils.reqCtxMapping(req, 'params', ['shortname', 'username']) + next() +} + function parseError (req, res, next) { const err = validationResult(req).formatWith(({ location, msg, param, value, nestedErrors }) => { return { msg: msg, param: param, location: location } @@ -413,12 +195,11 @@ module.exports = { parsePutParams, parsePostParams, parseGetParams, + parseDeleteParams, parseError, isOrgRole, isUserRole, isValidUsername, - validateCreateOrgParameters, validateUpdateOrgParameters, - validateUserIdOrUsername, shortCircuitLegacyCpsMitreOrgParameters } diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js deleted file mode 100644 index aa9acc93b..000000000 --- a/src/controller/registry-org.controller/index.js +++ /dev/null @@ -1,552 +0,0 @@ -const express = require('express') -const router = express.Router() -const mw = require('../../middleware/middleware') -const { param, query, body } = require('express-validator') -const controller = require('./registry-org.controller') -const { parseGetParams, parsePostParams, parseDeleteParams, parseError } = require('./registry-org.middleware') -const getConstants = require('../../constants').getConstants -const CONSTANTS = getConstants() - -router.get('/registryOrg', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'getAllRegistryOrgs' - #swagger.ignore = true - #swagger.summary = "Retrieves information about all registry organizations (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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/list-registry-orgs-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.ignore = true - #swagger.summary = "Retrieves information about a specific registry organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: 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/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[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(['identifier']).isString().trim(), - parseError, - parseGetParams, - controller.SINGLE_ORG -) - -router.post('/registryOrg', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'createRegistryOrg' - #swagger.ignore = true - #swagger.summary = "Creates a new registry organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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: '../schemas/registry-org/create-registry-org-request.json' } - } - } - } - #swagger.responses[201] = { - description: 'The registry organization was successfully created', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-org/create-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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - body(['reports_to']).not().exists().withMessage('reports_to must not be present'), - parseError, - parsePostParams, - controller.CREATE_ORG -) - -router.put('/registryOrg/:shortname', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'updateRegistryOrg' - #swagger.ignore = true - #swagger.summary = "Updates an existing registry organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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: '../schemas/registry-org/update-registry-org-request.json' } - } - } - } - #swagger.responses[200] = { - description: 'The registry organization was successfully updated', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-org/update-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[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - param(['shortname']).isString().trim(), - body(['reports_to']).not().exists().withMessage('reports_to must not be present'), - parseError, - parsePostParams, - controller.UPDATE_ORG -) - -router.delete( - '/registryOrg/:identifier', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'deleteRegistryOrg' - #swagger.ignore = true - #swagger.summary = "Deletes an existing registry organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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: { - type: 'object', - properties: { - message: { - type: 'string', - description: 'Message describing successful deletion operation' - } - } - } - } - } - } - #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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - param(['identifier']).isString().trim(), - parseError, - parseDeleteParams, - controller.DELETE_ORG -) - -router.get('/registryOrg/:shortname/users', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserOrgAll' - #swagger.ignore = true - #swagger.summary = "Retrieves all users for the organization with the specified short name (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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/registry-user/list-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[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - 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.ignore = true - #swagger.summary = "Create a user with the provided short name as the owning organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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/registry-user/create-registry-user-request.json' }, - } - } - } - #swagger.responses[200] = { - description: 'Returns the new user information (with the secret)', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/create-registry-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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - - parseError, - parsePostParams, - controller.USER_CREATE_SINGLE) - -module.exports = router diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js deleted file mode 100644 index 2d79faf6a..000000000 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ /dev/null @@ -1,74 +0,0 @@ -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) { - req.body = utils.deepRemoveEmpty(req.body) - 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', - 'oversees', - 'top_level_root', 'users', - 'charter_or_scope', 'disclosure_policy', 'product_list', - 'id_quota', - 'private_contacts', 'contact_info.websites', 'contact_info.emails', 'contact_info.phone', - 'partner_role_type', - 'partner_number', - 'partner_country', - 'program_data.cve_website_update_date', - 'program_data.cve_website_update_needed', - 'program_data.status', - 'advisory_locations', - 'advisory_location_require_credentials', - 'vulnerability_advisory_location_for_web_scraping', - 'industry', - 'tl_root_start_date', - 'is_cna_discussion_list' - ]) - 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 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 deleted file mode 100644 index 179987a83..000000000 --- a/src/controller/registry-user.controller/index.js +++ /dev/null @@ -1,397 +0,0 @@ -const express = require('express') -const router = express.Router() -const mw = require('../../middleware/middleware') -const { 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 = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'getAllRegistryUsers' - #swagger.ignore = true - #swagger.summary = "Retrieves information about all registry users (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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 users, along with pagination fields if results span multiple pages of data', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/list-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 = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'getSingleRegistryUser' - #swagger.ignore = true - #swagger.summary = "Retrieves information about a specific registry user (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: 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/registry-user/get-registry-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, - mw.onlySecretariat, - param(['identifier']).isString().trim(), - parseError, - parseGetParams, - controller.SINGLE_USER -) - -router.post('/registryUser/:shortname', - /* - #swagger.tags = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'createRegistryUser' - #swagger.ignore = true - #swagger.summary = "Creates a new registry user (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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: '../schemas/registry-user/create-registry-user-request.json' } - } - } - } - #swagger.responses[201] = { - description: 'The registry user was successfully created', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/create-registry-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, - param(['shortname']).isString().trim(), - parseError, - parsePostParams, - controller.CREATE_USER -) - -router.put('/registryUser/:identifier', - /* - #swagger.tags = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'updateRegistryUser' - #swagger.ignore = true - #swagger.summary = "Updates an existing registry user (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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: '../schemas/registry-user/update-registry-user-request.json' } - } - } - } - #swagger.responses[200] = { - description: 'The registry user was successfully updated', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/update-registry-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.onlySecretariat, - param(['identifier']).isString().trim(), - // TODO: do more validation here - parseError, - parsePostParams, - controller.UPDATE_USER -) - -router.delete( - '/registryUser/:identifier', - /* - #swagger.tags = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'deleteRegistryUser' - #swagger.ignore = true - #swagger.summary = "Deletes an existing registry user (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

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: { - type: 'object', - properties: { - message: { - type: 'string', - description: 'Message describing successful deletion operation' - } - } - } - } - } - } - #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(['identifier']).isString().trim(), - parseError, - parseDeleteParams, - controller.DELETE_USER -) - -module.exports = router diff --git a/src/controller/registry-user.controller/registry-user.middleware.js b/src/controller/registry-user.controller/registry-user.middleware.js deleted file mode 100644 index cffcb7e6f..000000000 --- a/src/controller/registry-user.controller/registry-user.middleware.js +++ /dev/null @@ -1,42 +0,0 @@ -const utils = require('../../utils/utils') -const { validationResult } = require('express-validator') -const errors = require('../registry-org.controller/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', [ - 'new_username', - 'name.first', 'name.last', 'name.middle', 'name.suffix' - ]) - 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() -} - -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, - parseDeleteParams, - parseError -} diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js new file mode 100644 index 000000000..2735f8cdf --- /dev/null +++ b/src/controller/registry.controller/index.js @@ -0,0 +1,1501 @@ +const express = require('express') +const router = express.Router() +const mw = require('../../middleware/middleware') +const errorMsgs = require('../../middleware/errorMessages') +const controller = require('../org.controller/org.controller') +const registryOrgController = require('./org.registry.controller') +const registryUserController = require('./user.registry.controller') +const { body, param, query } = require('express-validator') +const { parseGetParams, parsePostParams, parsePutParams, parseDeleteParams, parseError, isUserRole, isValidUsername } = require('../org.controller/org.middleware') +// Only God and Javascript know swhy its saying it is not used when it is..... +// eslint-disable-next-line no-unused-vars +const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') +const getConstants = require('../../../src/constants').getConstants +const CONSTANTS = getConstants() +const { parseGetParams: parseUserGetParams, parseError: parseUserError } = require('../user.controller/user.middleware') + +router.get('/registry/org', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgAll' + #swagger.summary = "Retrieves all registry organizations (accessible to Secretariat)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves information about all registry organizations

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/pageQuery', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns information about all registry organizations, along with pagination fields if results span multiple pages of data', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-org/list-registry-orgs-response.json' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), + parseError, + parseGetParams, + registryOrgController.ALL_ORGS +) + +router.get('/registry/org/:shortname/users', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryOrgUsersAll' + #swagger.summary = "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

+

Expected Behavior

+

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/registry-user/list-registry-users-response.json' + }, + example: { + "totalCount": 1, + "itemsPerPage": 100, + "pageCount": 1, + "currentPage": 1, + "prevPage": null, + "nextPage": null, + "users": [ + { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "role": "ADMIN", + "status": "active", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" + } + ] + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), + parseError, + parseGetParams, + registryOrgController.USER_ALL) + +router.get('/registry/org/:shortname/id_quota', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgIdQuota' + #swagger.summary = "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

+

Expected Behavior

+

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

+

Secretariat: Retrieves the CVE ID quota for any organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the CVE ID quota for an organization', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-org/get-registry-org-quota-response.json' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), + parseError, + parseGetParams, + controller.ORG_ID_QUOTA) + +router.get('/registry/org/:identifier', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgSingle' + #swagger.summary = "Retrieves information about the registry organization specified by short name or UUID (accessible to same-organization users or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

+

Expected Behavior

+

Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization

+

Secretariat: Retrieves information about any registry organization

" + #swagger.parameters['identifier'] = { description: 'The shortname or UUID of the registry organization' } + #swagger.parameters['expand'] = { + in: 'query', + description: 'Optional expanded related data. Accepted value: users.', + required: false, + schema: { + type: 'string', + enum: ['users'] + } + } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the registry organization information', + 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[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['expand']) }), + query(['expand']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['expand']).optional().isIn(['users']), + parseError, + parseGetParams, + registryOrgController.SINGLE_ORG +) + +router.get('/registry/org/:shortname/user/:username', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserSingle' + #swagger.summary = "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

+

Expected Behavior

+

Regular, CNA & Admin Users: Retrieves information about a registry user in the same organization

+

Secretariat: Retrieves any registry user's information

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.parameters['shortname'] = { + description: 'The shortname of the organization' + } + #swagger.parameters['username'] = { + description: 'The username of the registry user', + schema: { + type: 'string', + pattern: '^[a-zA-Z0-9._@-]+$' + } + } + #swagger.responses[200] = { + description: 'Returns information about the specified registry user', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-user/get-registry-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.useRegistry(), + mw.validateUser, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + param(['username']).isString().trim().notEmpty().custom(isValidUsername), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), + parseError, + parseGetParams, + registryUserController.SINGLE_USER +) + +router.post('/registry/org', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgCreateSingle' + #swagger.summary = "Creates an organization (accessible to Secretariat)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Creates a new organization

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + anyOf: [ + { $ref: '../schemas/registry-org/SecretariatOrg.json' }, + { $ref: '../schemas/registry-org/CNAOrg.json' }, + { $ref: '../schemas/registry-org/ADPOrg.json' }, + { $ref: '../schemas/registry-org/BulkDownloadOrg.json' } + ] + }, + example: { + short_name: 'fake_company', + long_name: 'Fake Company', + id_quota: 1000, + authority: ['CNA'] + } + } + } + } + #swagger.responses[200] = { + description: 'Returns information about all organizations, along with pagination fields if results span multiple pages of data', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-org/list-registry-orgs-response.json' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), + parsePostParams, + parseError, + registryOrgController.CREATE_ORG +) + +router.put('/registry/org/:shortname', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgUpdateSingle' + #swagger.summary = "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

+

With Joint Approval required for the following fields:

+

Expected Behavior

+ This endpoint expects a full organization object in the request body. +

Secretariat: Updates any organization's information

+

Organization Admin: Requests changes to its organization's information

+ " + #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/registry-org/update-registry-org-request.json' + }, + example: { + short_name: 'fake_company', + long_name: 'Fake Company', + id_quota: 1000, + authority: ['CNA'] + } + } + } + } + #swagger.responses[200] = { + description: 'Returns information about the organization updated', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-org/update-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[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + // mw.onlySecretariat, + parseError, + parsePutParams, + registryOrgController.UPDATE_ORG +) + +router.delete('/registry/org/:shortname', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgDeleteSingle' + #swagger.summary = "Deletes the registry organization specified by short name (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Deletes the specified registry organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the registry organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Confirms deletion of the registry organization', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-org/delete-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[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + parseError, + parseDeleteParams, + registryOrgController.DELETE_ORG +) + +router.post('/registry/org/: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 Secretariat or target organization Admin)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be an Admin of the target organization

+

Expected Behavior

+

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/registry-user/create-registry-user-request.json' } + }, + example: { + "username": "jdoe", + "status": "active", + "name": { + "first": "John", + "last": "Doe" + } + } + } + } + #swagger.responses[200] = { + description: 'Returns the new user information (with the secret)', + content: { + "application/json": { + schema: + { $ref: '../schemas/registry-user/create-registry-user-response.json' } + }, + example: { + "message": "jdoe was successfully created.", + "created": { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "status": "active", + "secret": "12345-abcde-67890", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariatOrAdmin, + mw.onlyOrgWithPartnerRole, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['org_uuid']).optional().isString().trim(), + body(['uuid']).optional().isString().trim(), + body(['name.first']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_FIRSTNAME_LENGTH }).withMessage(errorMsgs.FIRSTNAME_LENGTH), + body(['name.last']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_LASTNAME_LENGTH }).withMessage(errorMsgs.LASTNAME_LENGTH), + body(['name.middle']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_MIDDLENAME_LENGTH }).withMessage(errorMsgs.MIDDLENAME_LENGTH), + body(['name.suffix']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_SUFFIX_LENGTH }).withMessage(errorMsgs.SUFFIX_LENGTH), + body(['authority.active_roles']).optional() + .custom(mw.isFlatStringArray) + .bail() + .customSanitizer(toUpperCaseArray) + .custom(isUserRole), + parseError, + parsePostParams, + registryOrgController.USER_CREATE_SINGLE +) + +router.put('/registry/org/:shortname/user/:username', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserUpdateSingle' + #swagger.summary = "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

+

Expected Behavior

+

Regular User: Updates the user's own information. Only name fields may be changed.

+

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

+

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/active', + '#/components/parameters/orgShortname', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the updated user information', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-user/update-registry-user-response.json' }, + example: { + "message": "jdoe was successfully updated.", + "updated": { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "status": "active", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" + } + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlyOrgWithPartnerRole, + parseError, + parsePutParams, + registryUserController.UPDATE_USER) + +router.delete('/registry/org/:shortname/user/:username', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserDeleteSingle' + #swagger.summary = "Deletes the registry user specified by organization and username (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Deletes the specified user from the specified organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Confirms deletion of the registry user', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-user/delete-registry-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.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + param(['username']).isString().trim().notEmpty().custom(isValidUsername), + parseError, + parseDeleteParams, + registryUserController.DELETE_USER +) + +router.put('/registry/org/:shortname/user/:username/reset_secret', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserResetSecret' + #swagger.summary = "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

+

Expected Behavior

+

Regular User: Resets user's own API secret

+

Admin User: Resets any user's API secret in the Admin's organization

+

Secretariat: Resets any user's API secret

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the new API key', + content: { + "application/json": { + schema: { $ref: '../schemas/user/reset-secret-response.json' } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlyOrgWithPartnerRole, + parseError, + parsePostParams, + controller.USER_RESET_SECRET +) + +router.post('/registry/org/:shortname/oversees/:shortname2/add', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgAddOverseeRelationship' + #swagger.summary = 'Assigns an organization to report to a ROOT organization (accessible to Secretariat only)' + #swagger.description = ' +

Access Control

+

User must belong to an organization with the Secretariat role.

+

Expected Behavior

+

The organization identified by shortname must have ROOT authority. The organization identified by shortname2 will + report to it. If shortname2 already reported to a different ROOT. It will be removed from that and both will receive an audit entry.

' + #swagger.parameters['shortname'] = { description: 'The shortname of the overseeing ROOT organization' } + #swagger.parameters['shortname2'] = { description: 'The shortname of the reporting organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Reports-to relationship added successfully', + content: { 'application/json': { schema: { type: 'object', properties: { message: { type: 'string' } } } } } + } + #swagger.responses[400] = { description: 'Bad Request', content: { 'application/json': { schema: { $ref: '../schemas/errors/bad-request.json' } } } } + #swagger.responses[401] = { description: 'Not Authenticated', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[403] = { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[404] = { description: 'Not Found', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[500] = { description: 'Internal Server Error', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + parseError, + parsePostParams, + registryOrgController.ADD_OVERSEE_RELATIONSHIP +) + +router.post('/registry/org/:shortname/oversees/:shortname2/remove', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgRemoveOverseeRelationship' + #swagger.summary = 'Removes an organization reports-to relationship (accessible to Secretariat only)' + #swagger.description = ' +

Access Control

+

User must belong to an organization with the Secretariat role.

+

Expected Behavior

+

The organization identified by shortname must have ROOT authority. The organization identified by shortname2 will no longer report to it.

' + #swagger.parameters['shortname'] = { description: 'The shortname of the overseeing ROOT organization' } + #swagger.parameters['shortname2'] = { description: 'The shortname of the reporting organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Reports-to relationship removed successfully', + content: { 'application/json': { schema: { type: 'object', properties: { message: { type: 'string' } } } } } + } + #swagger.responses[400] = { description: 'Bad Request', content: { 'application/json': { schema: { $ref: '../schemas/errors/bad-request.json' } } } } + #swagger.responses[401] = { description: 'Not Authenticated', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[403] = { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[404] = { description: 'Not Found', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[500] = { description: 'Internal Server Error', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + parseError, + parsePostParams, + registryOrgController.REMOVE_OVERSEE_RELATIONSHIP +) + +router.post('/registry/org/:shortname/user/:username/grant-role', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserGrantRole' + #swagger.summary = "Grants a role to a user (accessible to Secretariat or Org Admin)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be an Admin of the target organization

+

Expected Behavior

+

Admin User: Grants a role to a user in the Admin's organization

+

Secretariat: Grants a role to a user in any organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + role: { + type: 'string', + enum: ['ADMIN'] + } + }, + required: ['role'] + } + } + } + } + #swagger.responses[200] = { + description: 'Role granted successfully', + content: { + "application/json": { + schema: { type: 'object', properties: { message: { type: 'string' } } } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + // mw.onlyOrgWithPartnerRole, // This might be too restrictive if we want Secretariat to do it for any org type + parseError, + parsePostParams, + registryUserController.GRANT_ROLE +) + +router.post('/registry/org/:shortname/user/:username/revoke-role', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserRevokeRole' + #swagger.summary = "Revokes a role from a user (accessible to Secretariat or Org Admin)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be an Admin of the target organization

+

Expected Behavior

+

Admin User: Revokes a role from a user in the Admin's organization

+

Secretariat: Revokes a role from a user in any organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + role: { + type: 'string', + enum: ['ADMIN'] + } + }, + required: ['role'] + } + } + } + } + #swagger.responses[200] = { + description: 'Role revoked successfully', + content: { + "application/json": { + schema: { type: 'object', properties: { message: { type: 'string' } } } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + // mw.onlyOrgWithPartnerRole, + parseError, + parsePostParams, + registryUserController.REVOKE_ROLE +) + +router.put('/registry/org/:shortname/conversation/:index', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgUpdateConversation' + #swagger.summary = "Update the conversation at the given index for the given organization (accessible to Secretariat or original same-organization author)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be the original author of the conversation in the same organization

+

Expected Behavior

+

Original Author: Allowed to update only the message body of a conversation posted by them

+

Secretariat: Allowed to update the message body and/or visibility of any conversation

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['index'] = { description: 'The index of the conversation to update' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the updated conversation', + content: { + "application/json": { + schema: { $ref: '../schemas/conversation/update-conversation-response.json' } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlyOrgWithPartnerRole, + parseError, + parsePostParams, + registryOrgController.EDIT_CONVERSATION +) + +router.get('/registry/users', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserAll' + #swagger.summary = "Retrieves information about all registered users (accessible to Secretariat)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves information about all users for all organizations

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/pageQuery', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns all users, along with pagination fields if results span multiple pages of data.', + content:{ + "application/json":{ + schema: { + $ref: '../schemas/registry-user/list-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[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + parseUserError, + parseUserGetParams, + registryUserController.ALL_USERS +) + +module.exports = router diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry.controller/org.error.js similarity index 94% rename from src/controller/registry-org.controller/error.js rename to src/controller/registry.controller/org.error.js index 1b350b417..c48ec8a77 100644 --- a/src/controller/registry-org.controller/error.js +++ b/src/controller/registry.controller/org.error.js @@ -1,5 +1,6 @@ const idrErr = require('../../utils/error') +/** Registry organization controller errors. */ class RegistryOrgControllerError extends idrErr.IDRError { orgDnePathParam (shortname) { // org const err = {} @@ -140,6 +141,13 @@ class RegistryOrgControllerError extends idrErr.IDRError { err.message = `The following fields can only be modified by the Secretariat: ${fields.join(', ')}.` return err } + + overseeingOrgMustBeRoot (shortname) { + const err = {} + err.error = 'OVERSEEING_ORG_MUST_BE_ROOT' + err.message = `The '${shortname}' organization must have ROOT authority to oversee another organization.` + return err + } } module.exports = { diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry.controller/org.registry.controller.js similarity index 89% rename from src/controller/registry-org.controller/registry-org.controller.js rename to src/controller/registry.controller/org.registry.controller.js index cf739a819..56ff62871 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry.controller/org.registry.controller.js @@ -1,13 +1,15 @@ +/** Registry organization route handlers. */ const mongoose = require('mongoose') const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') const _ = require('lodash') -const errors = require('./error') +const errors = require('./org.error') const error = new errors.RegistryOrgControllerError() const conversationErrors = require('../conversation.controller/error') const convoError = new conversationErrors.ConversationControllerError() const validateUUID = require('uuid').validate const authContext = require('../../utils/authContext') +const { REGISTRY_FORMAT } = require('../format.constants') function addUUIDsToSet (uuidSet, values) { if (!Array.isArray(values)) return @@ -106,7 +108,7 @@ function removeAdditionalContactUUIDFields (org) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It retrieves a list of all registry organizations. - * Called by GET /api/registryOrg + * Called by GET /api/registry/org */ async function getAllOrgs (req, res, next) { try { @@ -127,7 +129,7 @@ async function getAllOrgs (req, res, next) { options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value try { - returnValue = await repo.getAllOrgs({ ...options }, false, isSecretariat) + returnValue = await repo.getAllOrgs({ ...options }, REGISTRY_FORMAT, isSecretariat) // fetch conversations for (let i = 0; i < returnValue.organizations.length; i++) { const conversation = await conversationRepo.getAllByTargetUUID(returnValue.organizations[i].UUID, isSecretariat) @@ -158,7 +160,7 @@ async function getAllOrgs (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It retrieves information about the specified registry organization. - * Called by GET /api/registryOrg/:identifier + * Called by GET /api/registry/org/:identifier */ async function getOrg (req, res, next) { try { @@ -180,7 +182,7 @@ async function getOrg (req, res, next) { logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) } - returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, false, isSecretariat) + returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, REGISTRY_FORMAT, isSecretariat) if (returnValue) { let userRepo @@ -243,7 +245,7 @@ async function getOrg (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It creates a new registry organization. - * Called by POST /api/registryOrg + * Called by POST /api/registry/org */ async function createOrg (req, res, next) { try { @@ -305,7 +307,7 @@ async function createOrg (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, repo, { session }) // Create the org – repo.createOrg will handle field mapping - createdOrg = await repo.createOrg(body, { session, upsert: true }, false, requestingUserUUID, isSecretariat) + createdOrg = await repo.createOrg(body, { session, upsert: true }, REGISTRY_FORMAT, requestingUserUUID, isSecretariat) await session.commitTransaction() } catch (createErr) { @@ -409,7 +411,7 @@ async function validateRequestedShortName (req, repo, body, shortName, session) * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It updates an existing registry organization. - * Called by PUT /api/registryOrg/:shortname + * Called by PUT /api/registry/org/:shortname */ async function updateOrg (req, res, next) { try { @@ -419,6 +421,9 @@ async function updateOrg (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const conversationRepo = req.ctx.repositories.getConversationRepository() const { conversation, ...body } = req.ctx.body + // oversees is managed exclusively by the dedicated reports-to endpoints. + // Ignore it during standard organization updates. + delete body.oversees let updatedOrg let jointApprovalRequired @@ -548,7 +553,15 @@ async function updateOrg (req, res, next) { // Update Org full will cause a write to the Conversations collection, to avoid a read-after-write issue, we need to get the previous conversation data first const previousConversation = await conversationRepo.getAllByTargetUUID(await repo.getOrgUUID(shortName, { session }), isSecretariat, { session }) || [] - updatedOrg = await repo.updateOrgFull(shortName, req.ctx.body, { session }, false, requestingUser.UUID, isAdmin, isSecretariat) + updatedOrg = await repo.updateOrgFull( + shortName, + conversation ? { ...body, conversation } : body, + { session }, + REGISTRY_FORMAT, + requestingUser.UUID, + isAdmin, + isSecretariat + ) jointApprovalRequired = _.get(updatedOrg, 'joint_approval_required', false) _.unset(updatedOrg, 'joint_approval_required') // append previous conversations to any conversations that are in the org already @@ -608,18 +621,18 @@ async function updateOrg (req, res, next) { * * @async * @function deleteOrg - * @param {object} req - The Express request object, containing the organization identifier in `req.ctx.params.identifier`. + * @param {object} req - The Express request object, containing the organization short name in `req.ctx.params.shortname`. * @param {object} res - The Express response object. * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It deletes an existing registry organization. - * Called by DELETE /api/registryOrg/:identifier + * Called by DELETE /api/registry/org/:shortname */ async function deleteOrg (req, res, next) { try { const session = await mongoose.startSession({ causalConsistency: false }) const repo = req.ctx.repositories.getBaseOrgRepository() - const shortName = req.ctx.params.identifier + const shortName = req.ctx.params.shortname let targetOrgUUID try { @@ -658,6 +671,75 @@ async function deleteOrg (req, res, next) { } } +/** + * Adds or removes a reports-to relationship for a ROOT organization. + * + * @param {object} req - The Express request object. + * @param {object} res - The Express response object. + * @param {function} next - The Express next function. + * @param {'add'|'remove'} action - The relationship operation to perform. + * @returns {Promise} A promise that resolves when the response is sent. + */ +async function updateOverseeRelationship (req, res, next, action) { + try { + const session = await mongoose.startSession({ causalConsistency: false }) + const overseeingOrgShortName = req.ctx.params.shortname + const reportingOrgShortName = req.params.shortname2 + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() + + try { + session.startTransaction({ readPreference: 'primary' }) + const overseeingOrg = await orgRepo.findOneByShortName(overseeingOrgShortName, { session }) + if (!overseeingOrg) { + await session.abortTransaction() + return res.status(404).json(error.orgDnePathParam(overseeingOrgShortName)) + } + + if (!overseeingOrg.authority?.includes('ROOT')) { + await session.abortTransaction() + return res.status(400).json(error.overseeingOrgMustBeRoot(overseeingOrgShortName)) + } + + const reportingOrg = await orgRepo.findOneByShortName(reportingOrgShortName, { session }) + if (!reportingOrg) { + await session.abortTransaction() + return res.status(404).json(error.orgDnePathParam(reportingOrgShortName)) + } + + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }) + if (action === 'add') { + await orgRepo.addOverseeRelationship(overseeingOrgShortName, reportingOrg.UUID, { session }, requestingUserUUID) + } else { + await orgRepo.removeOverseeRelationship(overseeingOrgShortName, reportingOrg.UUID, { session }, requestingUserUUID) + } + + await session.commitTransaction() + } catch (updateErr) { + await session.abortTransaction() + throw updateErr + } finally { + await session.endSession() + } + + const message = action === 'add' + ? `${reportingOrgShortName} organization now reports to ${overseeingOrgShortName}.` + : `${reportingOrgShortName} organization no longer reports to ${overseeingOrgShortName}.` + logger.info({ uuid: req.ctx.uuid, message }) + return res.status(200).json({ message }) + } catch (err) { + next(err) + } +} + +async function addOverseeRelationship (req, res, next) { + return updateOverseeRelationship(req, res, next, 'add') +} + +async function removeOverseeRelationship (req, res, next) { + return updateOverseeRelationship(req, res, next, 'remove') +} + /** * Retrieves all users for the organization with the specified short name. * @@ -668,7 +750,7 @@ async function deleteOrg (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. Response body includes 'role' field for admins. * @description This endpoint is accessible to Secretariat only. It retrieves user information for any organization. - * Called by GET /api/registryOrg/:shortname/users + * Called by GET /api/registry/org/:shortname/users */ async function getUsers (req, res, next) { try { @@ -701,7 +783,7 @@ async function getUsers (req, res, next) { } // This should always return Registry typed - const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, true) + const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, REGISTRY_FORMAT) // Hydrate the role field const org = await orgRepo.findOneByShortName(orgShortName) @@ -726,7 +808,7 @@ async function getUsers (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It creates a user for any organization. - * Called by POST /api/registryOrg/:shortname/user + * Called by POST /api/registry/org/:shortname/user */ async function createUserByOrg (req, res, next) { try { @@ -737,7 +819,7 @@ async function createUserByOrg (req, res, next) { let returnValue // Check to make sure Org Exists first - const orgUUID = await orgRepo.getOrgUUID(orgShortName, {}, false) + const orgUUID = await orgRepo.getOrgUUID(orgShortName, {}, REGISTRY_FORMAT) if (!orgUUID) { logger.info({ uuid: req.ctx.uuid, message: 'The user could not be created because ' + orgShortName + ' organization does not exist.' }) return res.status(404).json(error.orgDnePathParam(orgShortName)) @@ -768,7 +850,7 @@ async function createUserByOrg (req, res, next) { } // Ask repo if user already exists - if (await userRepo.orgHasUser(orgShortName, body?.username, { session }, true)) { + if (await userRepo.orgHasUser(orgShortName, body?.username, { session }, REGISTRY_FORMAT)) { logger.info({ uuid: req.ctx.uuid, message: `${body?.username} user was not created because it already exists.` }) await session.abortTransaction() return res.status(400).json(error.userExists(body?.username)) @@ -788,7 +870,7 @@ async function createUserByOrg (req, res, next) { } const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }) - returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, true, requestingUserUUID) + returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, REGISTRY_FORMAT, requestingUserUUID) await session.commitTransaction() } catch (error) { await session.abortTransaction() @@ -848,7 +930,7 @@ async function editConversationForOrg (req, res, next) { const session = await mongoose.startSession({ causalConsistency: false }) try { - const orgUUID = await repo.getOrgUUID(orgShortName, {}, false) + const orgUUID = await repo.getOrgUUID(orgShortName, {}, REGISTRY_FORMAT) if (!orgUUID) { await session.endSession() return res.status(404).json(error.orgDnePathParam(orgShortName)) @@ -927,6 +1009,8 @@ module.exports = { CREATE_ORG: createOrg, UPDATE_ORG: updateOrg, DELETE_ORG: deleteOrg, + ADD_OVERSEE_RELATIONSHIP: addOverseeRelationship, + REMOVE_OVERSEE_RELATIONSHIP: removeOverseeRelationship, USER_ALL: getUsers, USER_CREATE_SINGLE: createUserByOrg, EDIT_CONVERSATION: editConversationForOrg diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry.controller/user.registry.controller.js similarity index 83% rename from src/controller/registry-user.controller/registry-user.controller.js rename to src/controller/registry.controller/user.registry.controller.js index 7502322f0..2ffddf442 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry.controller/user.registry.controller.js @@ -1,3 +1,4 @@ +/** Registry user controller route handlers. */ const mongoose = require('mongoose') const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') @@ -6,6 +7,7 @@ const error = new errors.UserControllerError() const validateUUID = require('uuid').validate const _ = require('lodash') const authContext = require('../../utils/authContext') +const { REGISTRY_FORMAT } = require('../format.constants') const immutableUpdateFields = ['created', 'last_updated'] @@ -25,7 +27,7 @@ function removeImmutableUpdateFields (body) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. Response body includes 'role' field for admins. * @description This endpoint is accessible to Secretariat only. It retrieves a list of all registry users. - * Called by GET /api/registryUser + * Called by GET /api/registry/users */ async function getAllUsers (req, res, next) { try { @@ -42,7 +44,7 @@ async function getAllUsers (req, res, next) { 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 returnValue = await repo.getAllUsers(options) + const returnValue = await repo.getAllUsers(options, REGISTRY_FORMAT) // Hydrate roles const orgRepo = req.ctx.repositories.getBaseOrgRepository() const distinctOrgUUIDs = [...new Set(returnValue.users.map(u => u.org_UUID))] @@ -54,7 +56,7 @@ async function getAllUsers (req, res, next) { const orgMap = {} for (const uuid of distinctOrgUUIDs) { // We need the org content to get admins - const org = await orgRepo.findOneByUUID(uuid) + const org = await orgRepo.findOneByUUID(uuid, {}, REGISTRY_FORMAT) if (org) { orgMap[uuid] = org } @@ -85,7 +87,7 @@ async function getAllUsers (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. Response body includes 'role' field for admins. * @description This endpoint is accessible to Secretariat only. It retrieves information about the specified registry user. - * Called by GET /api/registryUser/:identifier + * Called by GET /api/registry/org/:shortname/user/:username */ async function getUser (req, res, next) { /* @@ -109,14 +111,14 @@ async function getUser (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const repo = req.ctx.repositories.getBaseOrgRepository() - const isSecretariat = await authContext.isRequesterSecretariat(req, repo) + const isSecretariat = await authContext.isRequesterSecretariat(req, repo, {}, REGISTRY_FORMAT) try { let result let org if (identifier) { - result = await userRepo.findUserByUUID(identifier) + result = await userRepo.findUserByUUID(identifier, {}, REGISTRY_FORMAT) if (!result) { logger.info({ uuid: req.ctx.uuid, message: identifier + ' user could not be found.' }) return res.status(404).json(error.userDne(identifier)) @@ -128,22 +130,22 @@ async function getUser (req, res, next) { return res.status(404).json(error.userDne(identifier)) } - org = await repo.findOneByUUID(orgUUID) + org = await repo.findOneByUUID(orgUUID, {}, REGISTRY_FORMAT) userToGetParameters = { org: org.short_name, username: result.username } } else { - org = await repo.findOneByShortName(req.ctx.params.shortname) + org = await repo.findOneByShortName(req.ctx.params.shortname, {}, REGISTRY_FORMAT) - const isSameOrg = await authContext.isRequesterSameOrg(req, repo, org) + const isSameOrg = await authContext.isRequesterSameOrg(req, repo, org, {}, REGISTRY_FORMAT) if (!isSecretariat && !isSameOrg) { logger.info({ uuid: req.ctx.uuid, message: userToGetParameters.org + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) } - result = await userRepo.findOneByUsernameAndOrgShortname(userToGetParameters.username, userToGetParameters.org) + result = await userRepo.findOneByUsernameAndOrgShortname(userToGetParameters.username, userToGetParameters.org, {}, REGISTRY_FORMAT) if (!result) { logger.info({ uuid: req.ctx.uuid, message: userToGetParameters.username + ' user could not be found.' }) return res.status(404).json(error.userDne(userToGetParameters.username)) @@ -156,7 +158,7 @@ async function getUser (req, res, next) { } if (identifier) { - const isSameOrg = await authContext.isRequesterSameOrg(req, repo, org) + const isSameOrg = await authContext.isRequesterSameOrg(req, repo, org, {}, REGISTRY_FORMAT) if (!isSecretariat && !isSameOrg) { 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()) @@ -174,79 +176,6 @@ async function getUser (req, res, next) { } } -async function createUser (req, res, next) { - try { - const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const userRepo = req.ctx.repositories.getBaseUserRepository() - const body = req.ctx.body - const orgShortName = req.ctx.params.shortname - let returnValue - - const orgUUID = await orgRepo.getOrgUUID(orgShortName) - if (!orgUUID) { - logger.info({ uuid: req.ctx.uuid, message: 'The user could not be created because ' + orgShortName + ' organization does not exist.' }) - return res.status(404).json(error.orgDnePathParam(orgShortName)) - } - - // Do not allow the user to pass in a UUID - if ((body?.UUID ?? null) || (body?.uuid ?? null)) { - return res.status(400).json(error.uuidProvided('user')) - } - - if ((body?.org_UUID ?? null) || (body?.org_uuid ?? null)) { - return res.status(400).json(error.uuidProvided('org')) - } - - const session = await mongoose.startSession({ causalConsistency: false }) - - try { - session.startTransaction({ readPreference: 'primary' }) - - const result = await userRepo.validateUser(body) - if (body?.role && typeof body?.role !== 'string') { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) - } - if (!result.isValid) { - logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) - } - - // Ask repo if user already exists - if (await userRepo.orgHasUser(orgShortName, body?.username, { session })) { - logger.info({ uuid: req.ctx.uuid, message: `${body?.username} user was not created because it already exists.` }) - await session.abortTransaction() - return res.status(400).json(error.userExists(body?.username)) - } - - const users = await userRepo.findUsersByOrgShortname(orgShortName, { session }) - if (users.length >= 100) { - await session.abortTransaction() - return res.status(400).json(error.userLimitReached()) - } - - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }) - returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, true, requestingUserUUID) - await session.commitTransaction() - } catch (error) { - await session.abortTransaction() - throw error - } finally { - await session.endSession() - } - - const responseMessage = { - message: `${body?.username} was successfully created.`, - created: returnValue - } - - return res.status(200).json(responseMessage) - } catch (err) { - next(err) - } -} - async function updateUser (req, res, next) { /* This function is a little bit overloaded ATM until future releases of CVE-Services @@ -285,7 +214,7 @@ async function updateUser (req, res, next) { username: req.ctx.params.username } - const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo) + const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, REGISTRY_FORMAT) // TODO: This will need to be atomic at some point like revoke or grant // Specific check for org_short_name (Secretariat only) @@ -293,7 +222,7 @@ async function updateUser (req, res, next) { let userToEdit let org if (identifier) { - userToEdit = await userRepo.findUserByUUID(identifier) + userToEdit = await userRepo.findUserByUUID(identifier, {}, REGISTRY_FORMAT) if (!userToEdit) { logger.info({ uuid: req.ctx.uuid, message: identifier + ' user could not be found.' }) return res.status(404).json(error.userDne(identifier)) @@ -305,20 +234,20 @@ async function updateUser (req, res, next) { return res.status(404).json(error.orgDnePathParam(identifier)) } - org = await orgRepo.findOneByUUID(orgUUID) + org = await orgRepo.findOneByUUID(orgUUID, {}, REGISTRY_FORMAT) userToEditParameters.org = org.short_name userToEditParameters.username = userToEdit.username } else { - userToEdit = await userRepo.findOneByUsernameAndOrgShortname(userToEditParameters.username, userToEditParameters.org) - org = await orgRepo.findOneByShortName(userToEditParameters.org) + userToEdit = await userRepo.findOneByUsernameAndOrgShortname(userToEditParameters.username, userToEditParameters.org, {}, REGISTRY_FORMAT) + org = await orgRepo.findOneByShortName(userToEditParameters.org, {}, REGISTRY_FORMAT) if (!org) { logger.info({ uuid: req.ctx.uuid, message: `Target organization ${userToEditParameters.org} does not exist.` }) return res.status(404).json(error.orgDnePathParam(userToEditParameters.org)) } } - const isAdmin = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, org) - const requesterUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo) + const isAdmin = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, org, {}, REGISTRY_FORMAT) + const requesterUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, {}, REGISTRY_FORMAT) // Allow existing UUIDs to be passed, but block any attempts to mutate them if (userToEdit) { @@ -339,7 +268,7 @@ async function updateUser (req, res, next) { } if (body.org_short_name) { - const targetOrg = await orgRepo.findOneByShortName(body.org_short_name) + const targetOrg = await orgRepo.findOneByShortName(body.org_short_name, {}, REGISTRY_FORMAT) if (!targetOrg) { logger.info({ uuid: req.ctx.uuid, message: `Target organization ${body.org_short_name} does not exist.` }) return res.status(404).json(error.orgDnePathParam(body.org_short_name)) @@ -356,7 +285,7 @@ async function updateUser (req, res, next) { return res.status(404).json(error.orgDnePathParam(userToEditParameters.org)) } - const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, org) + const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, org, {}, REGISTRY_FORMAT) if (!isSecretariat && !isAdmin && !requesterSameOrg) { logger.info({ uuid: req.ctx.uuid, message: requestingUserParameters.org + ' user can only be updated by the user or admins of the same organization or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) @@ -413,7 +342,7 @@ async function updateUser (req, res, next) { // Ask repo if user already exists if (body?.username && body.username !== userToEdit.username) { - if (await userRepo.orgHasUser(userToEditParameters.org, body.username, { session })) { + if (await userRepo.orgHasUser(userToEditParameters.org, body.username, { session }, REGISTRY_FORMAT)) { logger.info({ uuid: req.ctx.uuid, message: 'The username ' + body.username + ' already exists.' }) await session.abortTransaction() return res.status(403).json(error.duplicateUsername()) @@ -424,7 +353,7 @@ async function updateUser (req, res, next) { const requestingUserUUID = requesterUserUUID updatedUserUUID = userToEdit.UUID - updatedUser = await userRepo.updateUserFull(userToEdit.UUID, body, { session }, true, requestingUserUUID) + updatedUser = await userRepo.updateUserFull(userToEdit.UUID, body, { session }, REGISTRY_FORMAT, requestingUserUUID) await session.commitTransaction() } catch (error) { await session.abortTransaction() @@ -454,20 +383,37 @@ async function updateUser (req, res, next) { } } +/** + * Deletes a registry user from the specified organization. + * Called by DELETE /api/registry/org/{shortname}/user/{username} + * + * @param {Object} req - The request object + * @param {Object} res - The response object + * @param {Function} next - The next middleware function + * @returns {Promise} + */ async function deleteUser (req, res, next) { try { const userRepo = req.ctx.repositories.getBaseUserRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const userUUID = req.ctx.params.identifier + const orgShortName = req.ctx.params.shortname + const username = req.ctx.params.username + const org = await orgRepo.findOneByShortName(orgShortName, {}, REGISTRY_FORMAT) - const user = await userRepo.findUserByUUID(userUUID) + if (!org) { + logger.info({ uuid: req.ctx.uuid, message: 'Org DNE' }) + return res.status(404).json(error.orgDnePathParam(orgShortName)) + } + + const user = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName, {}, REGISTRY_FORMAT) if (!user) { logger.info({ uuid: req.ctx.uuid, message: 'User DNE' }) - return res.status(404).json(error.userDne(userUUID)) + return res.status(404).json(error.userDne(username)) } - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo) + const userUUID = user.UUID + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, {}, REGISTRY_FORMAT) await userRepo.deleteUserByUUID(userUUID, {}, requestingUserUUID) const payload = { @@ -514,21 +460,21 @@ async function grantRole (req, res, next) { return res.status(404).json(error.orgDnePathParam(orgShortName)) } - const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo) + const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, REGISTRY_FORMAT) const targetOrg = { UUID: targetOrgUUID, short_name: orgShortName } - const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, targetOrg) + const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, targetOrg, {}, REGISTRY_FORMAT) if (!requesterSameOrg && !isSecretariat) { return res.status(403).json(error.notSameOrgOrSecretariat()) } - const isAdmin = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, targetOrg) + const isAdmin = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, targetOrg, {}, REGISTRY_FORMAT) if (!isSecretariat && !isAdmin) { return res.status(403).json(error.notOrgAdminOrSecretariatUpdate()) } // Check if target user exists in target org - const targetUser = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName) + const targetUser = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName, {}, REGISTRY_FORMAT) if (!targetUser) { return res.status(404).json(error.userDne(username)) } @@ -537,7 +483,7 @@ async function grantRole (req, res, next) { try { session.startTransaction({ readPreference: 'primary' }) - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }) + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, REGISTRY_FORMAT) await orgRepo.addAdmin(orgShortName, targetUser.UUID, { session }, requestingUserUUID) await session.commitTransaction() } catch (error) { @@ -579,27 +525,27 @@ async function revokeRole (req, res, next) { return res.status(404).json(error.orgDnePathParam(orgShortName)) } - const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo) + const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, REGISTRY_FORMAT) const targetOrg = { UUID: targetOrgUUID, short_name: orgShortName } - const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, targetOrg) + const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, targetOrg, {}, REGISTRY_FORMAT) if (!requesterSameOrg && !isSecretariat) { return res.status(403).json(error.notSameOrgOrSecretariat()) } - const isAdmin = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, targetOrg) + const isAdmin = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, targetOrg, {}, REGISTRY_FORMAT) if (!isSecretariat && !isAdmin) { return res.status(403).json(error.notOrgAdminOrSecretariatUpdate()) } // Check if target user exists in target org - const targetUser = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName) + const targetUser = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName, {}, REGISTRY_FORMAT) if (!targetUser) { return res.status(404).json(error.userDne(username)) } // Prevent Self-Demotion - const callingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo) + const callingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, {}, REGISTRY_FORMAT) if (callingUserUUID === targetUser.UUID) { return res.status(403).json({ error: 'NOT_ALLOWED_TO_SELF_DEMOTE', message: 'You cannot remove the ADMIN role from yourself.' }) } @@ -608,7 +554,7 @@ async function revokeRole (req, res, next) { try { session.startTransaction({ readPreference: 'primary' }) - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }) + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, REGISTRY_FORMAT) await orgRepo.removeAdmin(orgShortName, targetUser.UUID, { session }, requestingUserUUID) await session.commitTransaction() } catch (error) { @@ -628,7 +574,6 @@ async function revokeRole (req, res, next) { module.exports = { ALL_USERS: getAllUsers, SINGLE_USER: getUser, - CREATE_USER: createUser, UPDATE_USER: updateUser, DELETE_USER: deleteUser, GRANT_ROLE: grantRole, diff --git a/src/controller/user.controller/index.js b/src/controller/user.controller/index.js index c7d4a73d8..c118669c0 100644 --- a/src/controller/user.controller/index.js +++ b/src/controller/user.controller/index.js @@ -1,95 +1,12 @@ const express = require('express') const router = express.Router() const mw = require('../../middleware/middleware') -const { query, param } = require('express-validator') +const { query } = require('express-validator') const controller = require('./user.controller') -const registryUserController = require('../registry-user.controller/registry-user.controller.js') const { parseGetParams, parseError } = require('./user.middleware') -// Only God and Javascript know why its saying it is not used when it is..... -// eslint-disable-next-line no-unused-vars -const { handleRegistryParameter } = require('../../middleware/middleware') const getConstants = require('../../constants').getConstants const CONSTANTS = getConstants() -router.get('/registry/users', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'userAll' - #swagger.summary = "Retrieves information about all registered users (accessible to Secretariat)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role

-

Expected Behavior

-

Secretariat: Retrieves information about all users for all organizations

" - #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/registry', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns all users, along with pagination fields if results span multiple pages of data.', - content:{ - "application/json":{ - schema: { - $ref: '../schemas/registry-user/list-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[404] = { - description: 'Not Found', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - #swagger.responses[500] = { - description: 'Internal Server Error', - content: { - "application/json": { - schema: { $ref: '../schemas/errors/generic.json' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - parseError, - parseGetParams, - registryUserController.ALL_USERS -) - router.get('/users', /* #swagger.tags = ['Users'] @@ -102,7 +19,6 @@ router.get('/users',

Secretariat: Retrieves information about all users for all organizations

" #swagger.parameters['$ref'] = [ '#/components/parameters/pageQuery', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -112,10 +28,7 @@ router.get('/users', content:{ "application/json":{ schema: { - oneOf: [ - { $ref: '../schemas/user/list-users-response.json' }, - { $ref: '../schemas/registry-user/list-registry-users-response.json' } - ] + $ref: '../schemas/user/list-users-response.json' } } } @@ -161,12 +74,11 @@ router.get('/users', } } */ - param(['registry']).optional().isBoolean(), - mw.handleRegistryParameter, mw.validateUser, mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - query(['page', 'registry']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['page']).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 3d78e4a43..a5a8f1a4a 100644 --- a/src/controller/user.controller/user.controller.js +++ b/src/controller/user.controller/user.controller.js @@ -2,6 +2,7 @@ require('dotenv').config() const logger = require('../../middleware/logger') const getConstants = require('../../constants').getConstants +const { LEGACY_FORMAT, REGISTRY_FORMAT } = require('../format.constants') /** * Get the details of all users @@ -22,7 +23,7 @@ async function getAllUsers (req, res, next) { 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 returnValue = await repo.getAllUsers(options, !!req.useRegistry) + const returnValue = await repo.getAllUsers(options, req.useRegistry ? REGISTRY_FORMAT : LEGACY_FORMAT) logger.info({ uuid: req.ctx.uuid, message: 'The user information was sent to the secretariat user.' }) return res.status(200).json(returnValue) diff --git a/src/controller/user.controller/user.middleware.js b/src/controller/user.controller/user.middleware.js index e9477fb70..95a900313 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', 'registry']) + utils.reqCtxMapping(req, 'query', ['page']) next() } diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index aa32a66b6..a02604c2e 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -96,11 +96,6 @@ async function optionallyValidateUser (req, res, next) { } } -const handleRegistryParameter = (req, res, next) => { - req.useRegistry = req.query.registry === 'true' - next() -} - const useRegistry = () => { return (req, res, next) => { req.useRegistry = true @@ -488,10 +483,6 @@ function isFlatStringArray (val) { return true } -function isCveProgramOrgMembershipObject (val) { - console.log(val) -} - /** * Recursively casts to strings and upper-cases all items in array * @@ -513,27 +504,6 @@ function toUpperCaseArray (val) { return newArr } -/** - * Recursively casts to strings and lower-cases all items in array - * - * @param {Array} val - */ -function toLowerCaseArray (val) { - if (!Array.isArray(val)) { - return val.toString().toLowerCase() - } - - const newArr = val.map(k => { - if (Array.isArray(k)) { - return toLowerCaseArray(k) - } else { - return k.toString().toLowerCase() - } - }) - - return newArr -} - // Check for the invalid characters <, >, and " function containsNoInvalidCharacters (val) { const invalidCharacterList = ['<', '>', '"'] @@ -547,32 +517,10 @@ function containsNoInvalidCharacters (val) { return true } -/** - * Middleware factory that rejects any keys in the request body - * that are not listed in the allowedKeys array. - * - * @param {Array} allowedKeys - List of permitted keys in req.body - * @returns {function} Express middleware - */ -function rejectUnexpectedKeys (allowedKeys) { - return (req, res, next) => { - const bodyKeys = Object.keys(req.body || {}) - const unexpected = bodyKeys.filter(k => !allowedKeys.includes(k)) - if (unexpected.length > 0) { - return res.status(400).json({ - error: 'Unexpected keys in request body', - unexpected - }) - } - next() - } -} - module.exports = { setCacheControl, optionallyValidateUser, validateUser, - handleRegistryParameter, useRegistry, onlySecretariat, onlySecretariatOrBulkDownload, @@ -588,10 +536,7 @@ module.exports = { validateJsonSyntax, rateLimiter: limiter, isFlatStringArray, - isCveProgramOrgMembershipObject, toUpperCaseArray, - toLowerCaseArray, containsNoInvalidCharacters, - trimJSONWhitespace, - rejectUnexpectedKeys + trimJSONWhitespace } diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index f155a1c01..8585ef2aa 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -179,7 +179,8 @@ class BaseOrgRepository extends BaseRepository { */ async findOneByShortNameWithSelect (shortName, select, options = {}, returnLegacyFormat = false) { const OrgRepository = require('./orgRepository') - if (returnLegacyFormat) return await OrgRepository.findOneByShortName(shortName, options) + const legacyOrgRepo = new OrgRepository() + if (returnLegacyFormat) return await legacyOrgRepo.findOneByShortName(shortName, options, select) return await BaseOrgModel.findOne({ short_name: shortName }, null, options).select(select) } @@ -461,6 +462,63 @@ class BaseOrgRepository extends BaseRepository { return updatedOrg } + /** + * @async + * @function addOverseeRelationship + * @description Assigns a reporting organization to a ROOT organization. A reporting organization can have only one overseeing organization. + * @param {string} overseeingOrgShortName - The short name of the ROOT organization. + * @param {string} reportingOrgUUID - The UUID of the organization that reports to the ROOT organization. + * @param {object} [options={}] - Optional settings for the repository query. + * @param {string|null} [requestingUserUUID=null] - The requester UUID used for audit documentation. + * @returns {Promise} The updated ROOT organization. + */ + async addOverseeRelationship (overseeingOrgShortName, reportingOrgUUID, options = {}, requestingUserUUID = null) { + const overseeingOrg = await RootOrgModel.findOne({ short_name: overseeingOrgShortName }, null, options) + const originalOverseeingOrg = overseeingOrg.toObject() + const previousOverseeingOrgs = await RootOrgModel.find({ oversees: reportingOrgUUID }, null, options) + + for (const previousOverseeingOrg of previousOverseeingOrgs) { + if (previousOverseeingOrg.UUID === overseeingOrg.UUID) continue + const originalPreviousOverseeingOrg = previousOverseeingOrg.toObject() + previousOverseeingOrg.oversees = previousOverseeingOrg.oversees.filter(uuid => uuid !== reportingOrgUUID) + if (requestingUserUUID) { + await createAuditLogEntry(previousOverseeingOrg, originalPreviousOverseeingOrg, requestingUserUUID, options) + } + await previousOverseeingOrg.save(options) + } + + overseeingOrg.oversees = [...new Set([...(overseeingOrg.oversees || []), reportingOrgUUID])] + if (requestingUserUUID) { + await createAuditLogEntry(overseeingOrg, originalOverseeingOrg, requestingUserUUID, options) + } + await overseeingOrg.save(options) + + return overseeingOrg + } + + /** + * @async + * @function removeOverseeRelationship + * @description Removes a reporting organization from a ROOT organization's oversee list. + * @param {string} overseeingOrgShortName - The short name of the ROOT organization. + * @param {string} reportingOrgUUID - The UUID of the organization that no longer reports to the ROOT organization. + * @param {object} [options={}] - Optional settings for the repository query. + * @param {string|null} [requestingUserUUID=null] - The requester UUID used for audit documentation. + * @returns {Promise} The updated ROOT organization. + */ + async removeOverseeRelationship (overseeingOrgShortName, reportingOrgUUID, options = {}, requestingUserUUID = null) { + const overseeingOrg = await RootOrgModel.findOne({ short_name: overseeingOrgShortName }, null, options) + const originalOverseeingOrg = overseeingOrg.toObject() + overseeingOrg.oversees = (overseeingOrg.oversees || []).filter(uuid => uuid !== reportingOrgUUID) + + if (requestingUserUUID) { + await createAuditLogEntry(overseeingOrg, originalOverseeingOrg, requestingUserUUID, options) + } + await overseeingOrg.save(options) + + return overseeingOrg + } + /** * @async * @function getAllOrgs @@ -744,6 +802,7 @@ class BaseOrgRepository extends BaseRepository { options ) } catch (auditError) { + console.error('Audit entry creation failed:', auditError) } } @@ -969,6 +1028,7 @@ class BaseOrgRepository extends BaseRepository { ) } } catch (auditError) { + console.error('Audit entry creation failed:', auditError) } } diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index 1875dd6c9..ae84f9764 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -83,10 +83,10 @@ class BaseUserRepository extends BaseRepository { * @param {string} orgShortName - The short name of the organization. * @param {string} uuid - The UUID of the user. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - Unused parameter. + * @param {boolean} [isLegacyObject=false] - Unused parameter. * @returns {Promise} True if the organization has the user, false otherwise. */ - async orgHasUserByUUID (orgShortName, uuid, options = {}, isRegistryObject = true) { + async orgHasUserByUUID (orgShortName, uuid, options = {}, isLegacyObject = false) { const org = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options) if (!org || !Array.isArray(org.users)) { return false @@ -103,10 +103,10 @@ class BaseUserRepository extends BaseRepository { * @param {string} orgShortName - The short name of the organization. * @param {string} username - The username to check. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - Unused parameter. + * @param {boolean} [isLegacyObject=false] - Unused parameter. * @returns {Promise} True if the organization has the user, false otherwise. */ - async orgHasUser (orgShortName, username, options = {}, isRegistryObject = true) { + async orgHasUser (orgShortName, username, options = {}, isLegacyObject = false) { // 1. Find the org const org = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options) if (!org || !Array.isArray(org.users)) { @@ -125,10 +125,10 @@ class BaseUserRepository extends BaseRepository { * @param {string} username - The username to find. * @param {string} orgShortName - The short name of the organization. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - If false, returns a legacy user object if found. + * @param {boolean} [isLegacyObject=false] - If true, returns a legacy user object if found. * @returns {Promise} The user object or null if not found. */ - async findOneByUsernameAndOrgShortname (username, orgShortName, options = {}, isRegistryObject = true) { + async findOneByUsernameAndOrgShortname (username, orgShortName, options = {}, isLegacyObject = false) { const legacyUserRepo = new UserRepository() const org = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options) if (!org || !Array.isArray(org.users)) { @@ -137,7 +137,7 @@ class BaseUserRepository extends BaseRepository { const user = await BaseUser.findOne({ username: username, UUID: { $in: org.users } }, null, options) - if (!isRegistryObject && user) { + if (isLegacyObject && user) { return await legacyUserRepo.findOneByUUID(user.UUID) || null } return user || null @@ -150,10 +150,10 @@ class BaseUserRepository extends BaseRepository { * @param {string} username - The username to find. * @param {string} orgUUID - The UUID of the organization. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - If false, returns a legacy user object if found. + * @param {boolean} [isLegacyObject=false] - If true, returns a legacy user object if found. * @returns {Promise} The user object or null if not found. */ - async findOneByUserNameAndOrgUUID (username, orgUUID, options = {}, isRegistryObject = true) { + async findOneByUserNameAndOrgUUID (username, orgUUID, options = {}, isLegacyObject = false) { const legacyUserRepo = new UserRepository() const org = await BaseOrgModel.findOne({ UUID: orgUUID }, null, options) if (!org || !Array.isArray(org.users)) { @@ -162,7 +162,7 @@ class BaseUserRepository extends BaseRepository { const user = await BaseUser.findOne({ username: username, UUID: { $in: org.users } }, null, options) - if (!isRegistryObject && user) { + if (isLegacyObject && user) { return await legacyUserRepo.findOneByUUID(user.UUID) || null } return user || null @@ -175,11 +175,11 @@ class BaseUserRepository extends BaseRepository { * @param {string} username - The username to find. * @param {string} orgUUID - The UUID of the organization. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - If false, returns a legacy user object if found. + * @param {boolean} [isLegacyObject=false] - If true, returns a legacy user object if found. * @returns {Promise} The user object or null if not found. */ - async findUserByUsernameAndOrgUUID (username, orgUUID, options = {}, isRegistryObject = true) { - return this.findOneByUserNameAndOrgUUID(username, orgUUID, options, isRegistryObject) + async findUserByUsernameAndOrgUUID (username, orgUUID, options = {}, isLegacyObject = false) { + return this.findOneByUserNameAndOrgUUID(username, orgUUID, options, isLegacyObject) } /** @@ -188,17 +188,17 @@ class BaseUserRepository extends BaseRepository { * @description Finds a user by UUID. * @param {string} uuid - The UUID to find. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - If false, returns a legacy user object if found. + * @param {boolean} [isLegacyObject=false] - If true, returns a legacy user object if found. * @returns {Promise} The user object or null if not found. */ - async findUserByUUID (uuid, options = {}, isRegistryObject = true) { + async findUserByUUID (uuid, options = {}, isLegacyObject = false) { const legacyUserRepo = new UserRepository() const user = await BaseUser.findOne({ UUID: uuid }, null, options) if (!user) { return null } - if (!isRegistryObject) { + if (isLegacyObject) { return await legacyUserRepo.findOneByUUID(user.UUID) || null } return user || null @@ -223,10 +223,10 @@ class BaseUserRepository extends BaseRepository { * @param {string} userUUID - The user UUID to check. * @param {string} orgUUID - The organization UUID to check. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - If false, retrieves the legacy user object for role fallback. + * @param {boolean} [isLegacyObject=false] - If true, retrieves the legacy user object for role fallback. * @returns {Promise} True if the user is an admin of the org, false otherwise. */ - async isUserAdminOfOrgUUID (userUUID, orgUUID, options = {}, isRegistryObject = true) { + async isUserAdminOfOrgUUID (userUUID, orgUUID, options = {}, isLegacyObject = false) { if (!userUUID || !orgUUID) { return false } @@ -244,7 +244,7 @@ class BaseUserRepository extends BaseRepository { return false } - const user = await this.findUserByUUID(userUUID, options, isRegistryObject) + const user = await this.findUserByUUID(userUUID, options, isLegacyObject) return user?.role === 'ADMIN' || (Array.isArray(user?.authority?.active_roles) && user.authority.active_roles.includes('ADMIN')) } @@ -292,11 +292,11 @@ class BaseUserRepository extends BaseRepository { * @param {string} username - The username. * @param {string} orgShortname - The short name of the organization. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - If false, checks for legacy user format compatibility. + * @param {boolean} [isLegacyObject=false] - If true, checks for legacy user format compatibility. * @returns {Promise} The user UUID or null if not found. */ - async getUserUUID (username, orgShortname, options = {}, isRegistryObject = true) { - const user = await this.findOneByUsernameAndOrgShortname(username, orgShortname, options, isRegistryObject) + async getUserUUID (username, orgShortname, options = {}, isLegacyObject = false) { + const user = await this.findOneByUsernameAndOrgShortname(username, orgShortname, options, isLegacyObject) if (user) { return user.UUID } @@ -337,10 +337,10 @@ class BaseUserRepository extends BaseRepository { * @param {string} username - The username to check. * @param {string} orgShortName - The short name of the organization. * @param {object} options - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - Unused parameter. + * @param {boolean} [isLegacyObject=false] - Unused parameter. * @returns {Promise} True if the user is an Admin, false otherwise. */ - async isAdmin (username, orgShortName, options, isRegistryObject = true) { + async isAdmin (username, orgShortName, options, isLegacyObject = false) { const baseOrgRepository = new BaseOrgRepository() const existingOrg = await baseOrgRepository.findOneByShortName(orgShortName) @@ -357,13 +357,13 @@ class BaseUserRepository extends BaseRepository { * @param {string} username - The username to check. * @param {string} requesterOrg - The organization of the requester. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - Unused parameter. + * @param {boolean} [isLegacyObject=false] - Unused parameter. * @returns {Promise} True if the user is an Admin or Secretariat, false otherwise. */ - async isAdminOrSecretariat (orgShortName, username, requesterOrg, options = {}, isRegistryObject = true) { + async isAdminOrSecretariat (orgShortName, username, requesterOrg, options = {}, isLegacyObject = false) { const baseOrgRepository = new BaseOrgRepository() const org = await baseOrgRepository.findOneByShortName(requesterOrg) - if (await baseOrgRepository.isSecretariat(org) || await this.isAdmin(username, orgShortName, options, isRegistryObject)) { + if (await baseOrgRepository.isSecretariat(org) || await this.isAdmin(username, orgShortName, options, isLegacyObject)) { return true } return false @@ -374,14 +374,14 @@ class BaseUserRepository extends BaseRepository { * @function getAllUsers * @description Retrieves all users with pagination. * @param {object} [options={}] - Pagination and query options. - * @param {boolean} [isRegistryObject=true] - If true, returns registry formatted users. + * @param {boolean} [isLegacyObject=false] - If true, returns legacy formatted users. * @returns {Promise} Paginated result containing users and metadata. */ - async getAllUsers (options = {}, isRegistryObject = true) { + async getAllUsers (options = {}, isLegacyObject = false) { const UserRepository = require('./userRepository') const userRepo = new UserRepository() let pg - if (!isRegistryObject) { + if (isLegacyObject) { const agt = setAggregateUserObj({}) pg = await userRepo.aggregatePaginate(agt, options) } else { @@ -407,10 +407,10 @@ class BaseUserRepository extends BaseRepository { * @param {string} orgShortName - The short name of the organization. * @param {object} incomingUser - The user object to create. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - If false, accepts legacy user object. + * @param {boolean} [isLegacyObject=false] - If true, accepts legacy user object. * @returns {Promise} The created user object (registry or legacy format). */ - async createUser (orgShortName, incomingUser, options = {}, isRegistryObject = true, requestingUserUUID = null) { + async createUser (orgShortName, incomingUser, options = {}, isLegacyObject = false, requestingUserUUID = null) { const { deepRemoveEmpty } = require('../utils/utils') // TO-DO: org_UUID is not necessarily the shortname. Is this info lost during conversion? let legacyObjectRaw = null @@ -427,13 +427,13 @@ class BaseUserRepository extends BaseRepository { // Allow user to provide initial status, default to active let isConsideredInactive = false - if (isRegistryObject && incomingUser.status === 'inactive') isConsideredInactive = true - if (!isRegistryObject && (incomingUser.active === false || String(incomingUser.active).toLowerCase() === 'false')) isConsideredInactive = true + if (!isLegacyObject && incomingUser.status === 'inactive') isConsideredInactive = true + if (isLegacyObject && (incomingUser.active === false || String(incomingUser.active).toLowerCase() === 'false')) isConsideredInactive = true // Get UUID of org, that is having the user added to it. const existingOrg = await baseOrgRepository.findOneByShortName(orgShortName) - if (!isRegistryObject) { + if (isLegacyObject) { legacyObjectRaw = incomingUser legacyObjectRaw.secret = secret legacyObjectRaw.active = !isConsideredInactive @@ -455,7 +455,7 @@ class BaseUserRepository extends BaseRepository { registryObject = await registryUserToSave.save(options) const registryObjectPlain = toPlainObject(registryObject) - if (isRegistryObject) { + if (!isLegacyObject) { const legacyRole = userHasAdminRole(incomingUser) ? 'ADMIN' : incomingUser.role legacyObjectRaw = this.convertRegistryToLegacy({ ...registryObjectPlain, role: legacyRole }) legacyObjectRaw.secret = secret @@ -466,7 +466,7 @@ class BaseUserRepository extends BaseRepository { await baseOrgRepository.addUserToOrg(orgShortName, incomingUser.UUID, (userHasAdminRole(incomingUser) || userHasAdminRole(legacyObjectRaw)), options, false, requestingUserUUID) - if (!isRegistryObject) { + if (isLegacyObject) { legacyObjectRaw.secret = randomKey legacyObjectRaw.org_UUID = existingOrg.UUID delete legacyObjectRaw._id @@ -491,10 +491,10 @@ class BaseUserRepository extends BaseRepository { * @param {string} orgShortname - The short name of the organization. * @param {object} incomingParameters - The parameters to update. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - If false, returns a legacy user object. + * @param {boolean} [isLegacyObject=false] - If true, returns a legacy user object. * @returns {Promise} The updated user object. */ - async updateUser (username, orgShortname, incomingParameters, options = {}, isRegistryObject = true, requestingUserUUID = null) { + async updateUser (username, orgShortname, incomingParameters, options = {}, isLegacyObject = false, requestingUserUUID = null) { const { deepRemoveEmpty } = require('../utils/utils') const baseOrgRepository = new BaseOrgRepository() const legacyUserRepo = new UserRepository() @@ -503,7 +503,7 @@ class BaseUserRepository extends BaseRepository { const originalRegistryOrg = registryOrg.toObject() const legacyUser = await legacyUserRepo.findOneByUserNameAndOrgUUID(username, registryOrg.UUID, null, options) - const registryUser = await this.findOneByUsernameAndOrgShortname(username, orgShortname, options, true) + const registryUser = await this.findOneByUsernameAndOrgShortname(username, orgShortname, options, false) if (!registryUser && !legacyUser) { throw new Error('User not found') @@ -607,7 +607,7 @@ class BaseUserRepository extends BaseRepository { if (legacyUser) await legacyUser.save(options) if (registryUser) await registryUser.save(options) - if (!isRegistryObject) { + if (isLegacyObject) { if (!legacyUser) throw new Error('Legacy record missing; cannot return legacy format.') const plainJavascriptLegacyUser = legacyUser.toObject() plainJavascriptLegacyUser.role = finalRoles[0] ?? '' @@ -633,10 +633,10 @@ class BaseUserRepository extends BaseRepository { * @param {string} identifier - The identifier (UUID) of the user. * @param {object} incomingUser - The full user object with updates. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - If false, accepts/returns legacy format. + * @param {boolean} [isLegacyObject=false] - If true, accepts/returns legacy format. * @returns {Promise} The updated user object. */ - async updateUserFull (identifier, incomingUser, options = {}, isRegistryObject = true, requestingUserUUID = null) { + async updateUserFull (identifier, incomingUser, options = {}, isLegacyObject = false, requestingUserUUID = null) { const legacyUserRepo = new UserRepository() const registryUser = await this.findUserByUUID(identifier, options) @@ -648,8 +648,8 @@ class BaseUserRepository extends BaseRepository { } const { ...incomingUserBody } = incomingUser - const legacyObjectRaw = isRegistryObject ? this.convertRegistryToLegacy(incomingUserBody) : incomingUserBody - const registryObjectRaw = isRegistryObject ? incomingUserBody : this.convertLegacyToRegistry(incomingUserBody) + const legacyObjectRaw = isLegacyObject ? incomingUserBody : this.convertRegistryToLegacy(incomingUserBody) + const registryObjectRaw = isLegacyObject ? this.convertLegacyToRegistry(incomingUserBody) : incomingUserBody const protectedFieldsRegistry = ['_id', 'UUID', '__v', 'secret', 'created', 'last_updated'] const protectedFieldsLegacy = ['_id', 'UUID', '__v', 'secret', 'time', 'org_UUID'] @@ -667,7 +667,7 @@ class BaseUserRepository extends BaseRepository { if (legacyUser) { updatedLegacyUser = legacyUser.overwrite(_.mergeWith(_.pick(legacyUser.toObject(), protectedFieldsLegacy), _.omit(legacyObjectRaw, protectedFieldsLegacy), skipNulls)) // Align status from incoming payload or resolved registry state - const targetStatus = registryUser ? updatedRegistryUser.status : (isRegistryObject ? registryObjectRaw.status : 'active') + const targetStatus = registryUser ? updatedRegistryUser.status : (isLegacyObject ? 'active' : registryObjectRaw.status) updatedLegacyUser.active = (targetStatus === 'active') } @@ -728,7 +728,7 @@ class BaseUserRepository extends BaseRepository { throw new Error('Failed to update user: ' + error.message) } - if (!isRegistryObject) { + if (isLegacyObject) { if (!updatedLegacyUser) throw new Error('Legacy record missing; cannot output legacy format.') const plain = updatedLegacyUser.toObject() delete plain._id; delete plain.__v; delete plain.secret @@ -748,16 +748,16 @@ class BaseUserRepository extends BaseRepository { * @param {string} username - The username. * @param {string} orgShortName - The short name of the organization. * @param {object} [options={}] - Optional settings for the repository query. - * @param {boolean} [isRegistryObject=true] - Unused parameter. + * @param {boolean} [isLegacyObject=false] - Unused parameter. * @returns {Promise} The new random secret key. */ - async resetSecret (username, orgShortName, options = {}, isRegistryObject = true) { + async resetSecret (username, orgShortName, options = {}, isLegacyObject = false) { const legacyUserRepo = new UserRepository() const baseOrgRepository = new BaseOrgRepository() const legOrgUUID = await baseOrgRepository.getOrgUUID(orgShortName, options, true) const legUser = await legacyUserRepo.findOneByUserNameAndOrgUUID(username, legOrgUUID, null, options) - const regUser = await this.findOneByUsernameAndOrgShortname(username, orgShortName, options, true) + const regUser = await this.findOneByUsernameAndOrgShortname(username, orgShortName, options, false) // Fail ONLY if the user is completely missing from both collections if (!legUser && !regUser) { @@ -845,10 +845,10 @@ class BaseUserRepository extends BaseRepository { * * @param {string} orgShortname - The short name of the organization. * @param {object} options - Pagination options (e.g., limit, page). - * @param {boolean} isRegistryObject - Whether to return users in the registry format. + * @param {boolean} isLegacyObject - Whether to return users in the legacy format. * @returns {Promise} An object containing the list of users and pagination details. */ - async getAllUsersByOrgShortname (orgShortname, options = {}, isRegistryObject = true) { + async getAllUsersByOrgShortname (orgShortname, options = {}, isLegacyObject = false) { const CONSTANTS = getConstants() const baseOrgRepository = new BaseOrgRepository() const userRepository = new UserRepository() @@ -857,7 +857,7 @@ class BaseUserRepository extends BaseRepository { let agt = {} let pg - if (!isRegistryObject) { + if (isLegacyObject) { agt = setAggregateUserObj({ org_UUID: org.UUID }) pg = await userRepository.aggregatePaginate(agt, options) } else { diff --git a/src/repositories/orgRepository.js b/src/repositories/orgRepository.js index 3eb5d7684..48f47ee93 100644 --- a/src/repositories/orgRepository.js +++ b/src/repositories/orgRepository.js @@ -47,8 +47,8 @@ class OrgRepository extends BaseRepository { return utils.isBulkDownload(shortName) } - async getAllOrgs () { - return this.collection.find() + async getAllOrgs (options = {}, projection = {}) { + return this.collection.find({}, projection, options) } async deleteOneByShortName (shortName, options = {}) { diff --git a/src/repositories/userRepository.js b/src/repositories/userRepository.js index 9cd152b28..346710878 100644 --- a/src/repositories/userRepository.js +++ b/src/repositories/userRepository.js @@ -8,7 +8,7 @@ class UserRepository extends BaseRepository { async getUserUUID (userName, orgUUID, options = {}) { const utils = require('../utils/utils') - return utils.getUserUUID(userName, orgUUID, options) + return utils.getUserUUID(userName, orgUUID, false, options) } async isAdmin (username, shortname, options = {}) { @@ -61,8 +61,8 @@ class UserRepository extends BaseRepository { return this.collection.findOneAndUpdate(filter, updatePayload, options) } - async getAllUsers () { - return this.collection.find() + async getAllUsers (options = {}, projection = {}) { + return this.collection.find({}, projection, options) } } diff --git a/src/routes.config.js b/src/routes.config.js index 9cf95cdc3..b914e243c 100644 --- a/src/routes.config.js +++ b/src/routes.config.js @@ -7,8 +7,7 @@ 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') +const RegistryController = require('./controller/registry.controller') const AuditController = require('./controller/audit.controller') const ConversationController = require('./controller/conversation.controller') const ReviewObjectController = require('./controller/review-object.controller') @@ -36,9 +35,7 @@ module.exports = async function configureRoutes (app) { app.use('/api/', CveIdController) app.use('/api/', SystemController) app.use('/api/', UserController) - // At this time, we have moved the crud operations to mirror the cve legacy endpoint just with /registry/ in them. In the future we may want these. - app.use('/api/', RegistryUserController) - app.use('/api/', RegistryOrgController) + app.use('/api/', RegistryController) app.use('/api/', ConversationController) app.use('/api/', ReviewObjectController) app.use('/api/', GlossaryController) diff --git a/src/scripts/runMocha.js b/src/scripts/runMocha.js new file mode 100644 index 000000000..178ab22d6 --- /dev/null +++ b/src/scripts/runMocha.js @@ -0,0 +1,67 @@ +const fs = require('fs') +const path = require('path') +const Mocha = require('mocha') + +function collectTestFiles (targetPath, recursive) { + const resolvedPath = path.resolve(targetPath) + const stat = fs.statSync(resolvedPath) + + if (stat.isFile()) { + return [resolvedPath] + } + + if (!stat.isDirectory()) { + return [] + } + + return fs.readdirSync(resolvedPath).flatMap(entry => { + const entryPath = path.join(resolvedPath, entry) + const entryStat = fs.statSync(entryPath) + + if (entryStat.isDirectory()) { + return recursive ? collectTestFiles(entryPath, recursive) : [] + } + + return entryPath.endsWith('.js') ? [entryPath] : [] + }) +} + +function parseArgs (argv) { + return argv.reduce((options, arg) => { + if (arg === '--recursive') { + options.recursive = true + } else if (arg === '--exit') { + options.exit = true + } else { + options.paths.push(arg) + } + + return options + }, { exit: false, paths: [], recursive: false }) +} + +async function run () { + const options = parseArgs(process.argv.slice(2)) + const mocha = new Mocha() + const testPaths = options.paths.length > 0 ? options.paths : ['test'] + + testPaths + .flatMap(testPath => collectTestFiles(testPath, options.recursive)) + .sort() + .forEach(testFile => mocha.addFile(testFile)) + + await mocha.loadFilesAsync() + + mocha.run(failures => { + process.exitCode = failures ? 1 : 0 + + if (options.exit) { + setImmediate(() => process.exit(process.exitCode)) + } + }) +} + +run().catch(err => { + console.error(err) + process.exitCode = 1 +}) diff --git a/src/scripts/test_data/postman/README.md b/src/scripts/test_data/postman/README.md new file mode 100644 index 000000000..b9613310e --- /dev/null +++ b/src/scripts/test_data/postman/README.md @@ -0,0 +1,51 @@ +# CVE Services Registry Postman Tests + +This folder contains a Postman collection and environment for exercising the registry API flow: + +- `cve-services-registry.postman_collection.json` +- `cve-services.postman_environment.json` + +## Default Setup + +The default environment assumes a local development database and API created with: + +```sh +npm run populate:dev; npm run migrate:dev; npm run dev +``` + +If you are targeting a different database or API host, update the imported environment values before running the collection. + +## Import Into Postman + +1. Open Postman. +2. Select **Import**. +3. Import `cve-services-registry.postman_collection.json`. +4. Import `cve-services.postman_environment.json`. +5. Select the imported **CVE Services Registry** environment. + +## Environment Values + +Confirm these environment values match the API you want to test: + +- `baseUrl` +- `apiUser` +- `apiOrg` +- `apiKey` + +The default values are intended for the local development setup above. + +## Run Sequentially + +Use the Postman Collection Runner rather than sending each request manually. + +1. Open the `CVE Services Registry Tests` collection. +2. Select **Run collection**. +3. Start with `Create Registry Org`. +4. Keep the collection order unchanged. +5. Run the collection with the `CVE Services Registry` environment selected. + +The collection uses `postman.setNextRequest` to move through the requests in order. If a setup request does not return the expected response, the runner stops instead of continuing with dependent requests. + +## CVE-ID Year + +The collection variable `registryCveYear` defaults to `2023`, which matches the pre-populated development data. Update that collection variable if the target database is configured for a different CVE-ID range year. diff --git a/src/scripts/test_data/postman/cve-services-registry.postman_collection.json b/src/scripts/test_data/postman/cve-services-registry.postman_collection.json new file mode 100644 index 000000000..e79e30e80 --- /dev/null +++ b/src/scripts/test_data/postman/cve-services-registry.postman_collection.json @@ -0,0 +1,1952 @@ +{ + "info": { + "name": "CVE Services Registry Tests", + "description": "Registry API tests for the flow previously covered by src/scripts/test_data/generate.js: create a CNA org, create users, and grant an admin role.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "['baseUrl', 'apiUser', 'apiOrg', 'apiKey'].forEach((key) => {", + " if (!pm.environment.get(key)) {", + " throw new Error(`${key} must be set in the selected Postman environment.`)", + " }", + "})" + ] + } + } + ], + "item": [ + { + "name": "Create Registry Org", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`.toLowerCase()", + "const orgShortName = `ptest_${runId}`.slice(0, 32)", + "const adminUsername = `admin_${runId}@postman-registry.example`", + "const userUsername = `user_${runId}@postman-registry.example`", + "const additionalUserUsername = `extra_${runId}@postman-registry.example`", + "", + "pm.collectionVariables.set('registryRunId', runId)", + "pm.collectionVariables.set('registryOrgShortName', orgShortName)", + "pm.collectionVariables.set('registryOrgLongName', `Postman Registry CNA ${runId} (fake)`)", + "pm.collectionVariables.set('registryAdminUsername', adminUsername)", + "pm.collectionVariables.set('registryAdminUsernameEncoded', encodeURIComponent(adminUsername))", + "pm.collectionVariables.set('registryUserUsername', userUsername)", + "pm.collectionVariables.set('registryUserUsernameEncoded', encodeURIComponent(userUsername))", + "pm.collectionVariables.set('registryAdditionalUserUsername', additionalUserUsername)", + "pm.collectionVariables.set('registryAdminUpdateWebsite', `https://postman-registry.example/admin-update/${runId}`)" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedShortName = pm.collectionVariables.get('registryOrgShortName')", + "const expectedLongName = pm.collectionVariables.get('registryOrgLongName')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.created && body.created.UUID && body.created.short_name === expectedShortName", + "postman.setNextRequest(canContinue ? 'Create Admin User' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes the created registry org', () => {", + " pm.expect(body).to.have.property('message', `${expectedShortName} organization was successfully created.`)", + " pm.expect(body).to.have.property('created')", + " pm.expect(body.created).to.have.property('UUID').that.is.a('string')", + " pm.expect(body.created).to.have.property('short_name', expectedShortName)", + " pm.expect(body.created).to.have.property('long_name', expectedLongName)", + " pm.expect(body.created).to.have.property('authority').that.includes('CNA')", + " pm.expect(body.created).to.have.property('id_quota', 500)", + "})", + "", + "if (canContinue) {", + " pm.collectionVariables.set('registryOrgUuid', body.created.UUID)", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"partner_number\": \"CNA-POSTMAN-REGISTRY-{{registryRunId}}\",\n \"top_level_root\": \"MITRE TLR\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"phone\": \"+1-555-210-3344\",\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"https://postman-registry.example\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"program_data\": {\n \"status\": \"active\",\n \"partner_active_date\": \"2015-03-15\",\n \"cve_website_update_needed\": false,\n \"cve_website_update_date\": \"2024-11-01\"\n },\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org" + ] + }, + "description": "Creates a representative CNA org from the old test data shape." + } + }, + { + "name": "Create Admin User", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedUsername = pm.collectionVariables.get('registryAdminUsername')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.created && body.created.UUID && body.created.username === expectedUsername && body.created.secret", + "postman.setNextRequest(canContinue ? 'Create Regular User' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes the created admin-pattern user', () => {", + " pm.expect(body).to.have.property('message', `${expectedUsername} was successfully created.`)", + " pm.expect(body).to.have.property('created')", + " pm.expect(body.created).to.have.property('UUID').that.is.a('string')", + " pm.expect(body.created).to.have.property('username', expectedUsername)", + " pm.expect(body.created).to.have.nested.property('name.first', 'Jane')", + " pm.expect(body.created).to.have.nested.property('name.last', 'Holloway')", + " pm.expect(body.created).to.have.property('status', 'active')", + " pm.expect(body.created).to.have.property('secret').that.is.a('string').and.is.not.empty", + "})", + "", + "if (canContinue) {", + " pm.collectionVariables.set('registryAdminUserUuid', body.created.UUID)", + " pm.collectionVariables.set('registryAdminOrg', pm.collectionVariables.get('registryOrgShortName'))", + " pm.collectionVariables.set('registryAdminApiKey', body.created.secret)", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": {\n \"first\": \"Jane\",\n \"last\": \"Holloway\"\n },\n \"status\": \"active\",\n \"username\": \"{{registryAdminUsername}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user" + ] + }, + "description": "Creates the user that will receive the ADMIN role." + } + }, + { + "name": "Create Regular User", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedUsername = pm.collectionVariables.get('registryUserUsername')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.created && body.created.UUID && body.created.username === expectedUsername && body.created.secret", + "postman.setNextRequest(canContinue ? 'Grant Admin Role' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes the created regular user', () => {", + " pm.expect(body).to.have.property('message', `${expectedUsername} was successfully created.`)", + " pm.expect(body).to.have.property('created')", + " pm.expect(body.created).to.have.property('UUID').that.is.a('string')", + " pm.expect(body.created).to.have.property('username', expectedUsername)", + " pm.expect(body.created).to.have.nested.property('name.first', 'Brian')", + " pm.expect(body.created).to.have.nested.property('name.last', 'Stokes')", + " pm.expect(body.created).to.have.property('status', 'active')", + " pm.expect(body.created).to.have.property('secret').that.is.a('string').and.is.not.empty", + "})", + "", + "if (canContinue) {", + " pm.collectionVariables.set('registryUserUuid', body.created.UUID)", + " pm.collectionVariables.set('registryUserOriginalApiKey', body.created.secret)", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": {\n \"first\": \"Brian\",\n \"last\": \"Stokes\"\n },\n \"status\": \"active\",\n \"username\": \"{{registryUserUsername}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user" + ] + }, + "description": "Creates a non-admin user from the same representative org." + } + }, + { + "name": "Grant Admin Role", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedUsername = pm.collectionVariables.get('registryAdminUsername')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const expectedMessage = `Role ADMIN granted to user ${expectedUsername}.`", + "const canContinue = pm.response.code === 200 && body.message === expectedMessage", + "postman.setNextRequest(canContinue ? 'Admin Cannot Set Secretariat-Only Org Fields' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response confirms the ADMIN grant', () => {", + " pm.expect(body).to.have.property('message', expectedMessage)", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"ADMIN\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryAdminUsernameEncoded}}/grant-role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryAdminUsernameEncoded}}", + "grant-role" + ] + }, + "description": "Grants ADMIN to the admin-pattern registry user." + } + }, + { + "name": "Admin Cannot Set Secretariat-Only Org Fields", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 403 && body.error === 'SECRETARIAT_ONLY'", + "postman.setNextRequest(canContinue ? 'Registry Org Update Rejects Additional Key' : null)", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})", + "", + "pm.test('response identifies secretariat-only fields', () => {", + " pm.expect(body).to.have.property('error', 'SECRETARIAT_ONLY')", + " pm.expect(body.message || '').to.include('program_data')", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"id_quota\": 500,\n \"program_data\": {\n \"status\": \"active\"\n }\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}" + ] + }, + "description": "Verifies an org admin cannot update fields reserved for Secretariat users." + } + }, + { + "name": "Registry Org Update Rejects Additional Key", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const firstError = body.errors && body.errors[0]", + "const canContinue = pm.response.code === 400 && body.message === 'Parameters were invalid' && firstError && firstError.message === 'must NOT have additional properties'", + "postman.setNextRequest(canContinue ? 'Registry User Create Rejects Additional Key' : null)", + "", + "pm.test('status is 400', () => {", + " pm.response.to.have.status(400)", + "})", + "", + "pm.test('response rejects the additional org key', () => {", + " pm.expect(body).to.have.property('message', 'Parameters were invalid')", + " pm.expect(firstError).to.have.property('message', 'must NOT have additional properties')", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"phone\": \"+1-555-210-3344\",\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"https://postman-registry.example\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\",\n \"test\": \"additional key not in schema\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}" + ] + }, + "description": "Verifies registry org updates reject keys outside the schema." + } + }, + { + "name": "Registry User Create Rejects Additional Key", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const firstError = body.errors && body.errors[0]", + "const canContinue = pm.response.code === 400 && body.message === 'Parameters were invalid' && firstError && firstError.message === 'must NOT have additional properties'", + "postman.setNextRequest(canContinue ? 'Update Own Registry Org as Admin' : null)", + "", + "pm.test('status is 400', () => {", + " pm.response.to.have.status(400)", + "})", + "", + "pm.test('response rejects the additional user key', () => {", + " pm.expect(body).to.have.property('message', 'Parameters were invalid')", + " pm.expect(firstError).to.have.property('message', 'must NOT have additional properties')", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": {\n \"first\": \"Extra\",\n \"last\": \"User\"\n },\n \"status\": \"active\",\n \"username\": \"{{registryAdditionalUserUsername}}\",\n \"test\": \"additional key not in schema\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user" + ] + }, + "description": "Verifies registry user creation rejects keys outside the schema." + } + }, + { + "name": "Update Own Registry Org as Admin", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedShortName = pm.collectionVariables.get('registryOrgShortName')", + "const expectedWebsite = pm.collectionVariables.get('registryAdminUpdateWebsite')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const updatedWebsites = body.updated && body.updated.contact_info && body.updated.contact_info.websites", + "const canContinue = pm.response.code === 200 && body.updated && body.updated.short_name === expectedShortName && Array.isArray(updatedWebsites) && updatedWebsites.includes(expectedWebsite)", + "postman.setNextRequest(canContinue ? 'Reset Regular User Secret as Admin' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('admin-auth update targets the created registry org', () => {", + " pm.expect(body).to.have.property('updated')", + " pm.expect(body.updated).to.have.property('short_name', expectedShortName)", + "})", + "", + "pm.test('admin-auth update includes the requested website', () => {", + " pm.expect(body.updated).to.have.nested.property('contact_info.websites')", + " pm.expect(body.updated.contact_info.websites).to.include(expectedWebsite)", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"phone\": \"+1-555-210-3344\",\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"{{registryAdminUpdateWebsite}}\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}" + ] + }, + "description": "Uses the created admin user's credentials to update that user's own organization." + } + }, + { + "name": "Reset Regular User Secret as Admin", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body['API-secret']", + "postman.setNextRequest(canContinue ? 'Old Regular User Key Is Rejected' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes a new API secret', () => {", + " pm.expect(body).to.have.property('API-secret').that.is.a('string').and.is.not.empty", + "})", + "", + "if (pm.response.code === 200 && body['API-secret']) {", + " pm.collectionVariables.set('registryUserApiKey', body['API-secret'])", + "}" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}/reset_secret", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}", + "reset_secret" + ] + }, + "description": "Uses the created admin user's credentials to reset the regular user's API secret." + } + }, + { + "name": "Old Regular User Key Is Rejected", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 401", + "postman.setNextRequest(canContinue ? 'Use Reset Regular User Key' : null)", + "", + "pm.test('status is 401', () => {", + " pm.response.to.have.status(401)", + "})", + "", + "pm.test('response rejects the old API key', () => {", + " pm.expect(body.error || 'UNAUTHORIZED').to.equal('UNAUTHORIZED')", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserOriginalApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}" + ] + }, + "description": "Verifies the regular user's original API key no longer works after reset_secret." + } + }, + { + "name": "Use Reset Regular User Key", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedUsername = pm.collectionVariables.get('registryUserUsername')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.username === expectedUsername", + "postman.setNextRequest(canContinue ? 'Regular User Cannot Reset Admin Secret' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('regular user can authenticate with the reset API key', () => {", + " pm.expect(body).to.have.property('username', expectedUsername)", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}" + ] + }, + "description": "Uses the reset regular user's credentials to retrieve that user's registry profile." + } + }, + { + "name": "Regular User Cannot Reset Admin Secret", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const canContinue = pm.response.code === 403", + "postman.setNextRequest(canContinue ? 'Regular User Cannot Grant Admin Role' : null)", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryAdminUsernameEncoded}}/reset_secret", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryAdminUsernameEncoded}}", + "reset_secret" + ] + }, + "description": "Verifies a regular user cannot reset the admin user's API secret." + } + }, + { + "name": "Regular User Cannot Grant Admin Role", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 403 && body.error === 'NOT_ORG_ADMIN_OR_SECRETARIAT_UPDATE'", + "postman.setNextRequest(canContinue ? 'Regular User Cannot Deactivate Self' : null)", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})", + "", + "pm.test('response identifies missing admin privileges', () => {", + " pm.expect(body).to.have.property('error', 'NOT_ORG_ADMIN_OR_SECRETARIAT_UPDATE')", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"ADMIN\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}/grant-role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}", + "grant-role" + ] + }, + "description": "Verifies a regular user cannot grant themselves the ADMIN role." + } + }, + { + "name": "Regular User Cannot Deactivate Self", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 400 && body.error === 'NOT_ALLOWED_TO_CHANGE_FIELD'", + "postman.setNextRequest(canContinue ? 'Created Admin Cannot Revoke Own Admin Role' : null)", + "", + "pm.test('status is 400', () => {", + " pm.response.to.have.status(400)", + "})", + "", + "pm.test('response rejects regular-user status changes', () => {", + " pm.expect(body).to.have.property('error', 'NOT_ALLOWED_TO_CHANGE_FIELD')", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"inactive\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}" + ] + }, + "description": "Verifies a regular user cannot deactivate their own account." + } + }, + { + "name": "Created Admin Cannot Revoke Own Admin Role", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 403 && body.error === 'NOT_ALLOWED_TO_SELF_DEMOTE'", + "postman.setNextRequest(canContinue ? 'Created Admin Cannot Grant Invalid Role' : null)", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})", + "", + "pm.test('response rejects admin self-demotion', () => {", + " pm.expect(body).to.have.property('error', 'NOT_ALLOWED_TO_SELF_DEMOTE')", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"ADMIN\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryAdminUsernameEncoded}}/revoke-role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryAdminUsernameEncoded}}", + "revoke-role" + ] + }, + "description": "Verifies the created admin cannot revoke their own ADMIN role." + } + }, + { + "name": "Created Admin Cannot Grant Invalid Role", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 400 && body.error === 'BAD_INPUT'", + "postman.setNextRequest(canContinue ? 'Reserve CVE-ID as Created Admin' : null)", + "", + "pm.test('status is 400', () => {", + " pm.response.to.have.status(400)", + "})", + "", + "pm.test('response rejects unsupported roles', () => {", + " pm.expect(body).to.have.property('error', 'BAD_INPUT')", + " pm.expect(body.message || '').to.include('Invalid role request')", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"MAGNANIMOUS\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}/grant-role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}", + "grant-role" + ] + }, + "description": "Verifies role grants reject unsupported role names." + } + }, + { + "name": "Reserve CVE-ID as Created Admin", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedYear = String(pm.collectionVariables.get('registryCveYear'))", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const reservedCveId = body.cve_ids && body.cve_ids[0] && body.cve_ids[0].cve_id", + "const canContinue = pm.response.code === 200 && reservedCveId", + "postman.setNextRequest(canContinue ? 'Get Reserved CVE-ID as Created Admin' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes one reserved CVE-ID', () => {", + " pm.expect(body).to.have.property('cve_ids').that.is.an('array').with.lengthOf(1)", + " pm.expect(reservedCveId).to.match(new RegExp(`^CVE-${expectedYear}-[0-9]{4,}$`))", + "})", + "", + "if (canContinue) {", + " pm.collectionVariables.set('registryReservedCveId', reservedCveId)", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/cve-id?amount=1&cve_year={{registryCveYear}}&short_name={{registryOrgShortName}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "cve-id" + ], + "query": [ + { + "key": "amount", + "value": "1" + }, + { + "key": "cve_year", + "value": "{{registryCveYear}}" + }, + { + "key": "short_name", + "value": "{{registryOrgShortName}}" + } + ] + }, + "description": "Uses the created admin user's credentials to reserve one CVE-ID for that user's own organization." + } + }, + { + "name": "Get Reserved CVE-ID as Created Admin", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedCveId = pm.collectionVariables.get('registryReservedCveId')", + "const expectedOrg = pm.collectionVariables.get('registryOrgShortName')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.cve_id === expectedCveId && body.owning_cna === expectedOrg", + "postman.setNextRequest(canContinue ? 'Created Admin Cannot Reserve CVE-ID for Another Org' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('owning admin can see reserved CVE-ID details', () => {", + " pm.expect(body).to.have.property('cve_id', expectedCveId)", + " pm.expect(body).to.have.property('state', 'RESERVED')", + " pm.expect(body).to.have.property('owning_cna', expectedOrg)", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/cve-id/{{registryReservedCveId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "cve-id", + "{{registryReservedCveId}}" + ] + }, + "description": "Retrieves the reserved CVE-ID as the owning organization admin." + } + }, + { + "name": "Created Admin Cannot Reserve CVE-ID for Another Org", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.collectionVariables.set('registryFlowCompleted', 'true')", + "postman.setNextRequest(pm.collectionVariables.get('legacyFlowCompleted') ? null : 'Create Legacy Org')", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/cve-id?amount=1&cve_year={{registryCveYear}}&short_name={{apiOrg}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "cve-id" + ], + "query": [ + { + "key": "amount", + "value": "1" + }, + { + "key": "cve_year", + "value": "{{registryCveYear}}" + }, + { + "key": "short_name", + "value": "{{apiOrg}}" + } + ] + }, + "description": "Verifies the created admin cannot reserve CVE-IDs for another organization." + } + }, + { + "name": "Legacy Org and User Format Contract", + "item": [ + { + "name": "Create Legacy Org", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "pm.collectionVariables.unset('legacyFlowCompleted')", + "pm.collectionVariables.unset('registryFlowCompleted')", + "const id = `${Date.now().toString(36)}${Math.random().toString(36).slice(2,8)}`", + "pm.collectionVariables.set('legacyOrgShortName', `plegacy_${id}`.slice(0,32))", + "pm.collectionVariables.set('legacyOrgName', `Postman Legacy CNA ${id}`)", + "pm.collectionVariables.set('legacyUsername', `user_${id}@postman-legacy.example`)", + "pm.collectionVariables.set('legacyUsernameEncoded', encodeURIComponent(pm.collectionVariables.get('legacyUsername')))", + "pm.collectionVariables.set('legacyOrgUuid', '')" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (body.created && body.created.UUID && body.created.name && !body.created.long_name)", + "postman.setNextRequest(ok ? 'Get Legacy Org' : null)", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " body.created && body.created.UUID && body.created.name && !body.created.long_name", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "url": "{{baseUrl}}/api/org", + "body": { + "mode": "raw", + "raw": "{\"short_name\":\"{{legacyOrgShortName}}\",\"name\":\"{{legacyOrgName}}\",\"authority\":{\"active_roles\":[\"CNA\"]},\"policies\":{\"id_quota\":500}}" + } + } + }, + { + "name": "Get Legacy Org", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (body.name === pm.collectionVariables.get('legacyOrgName') && body.policies && body.policies.id_quota === 500 && !body.long_name)", + "postman.setNextRequest(ok ? 'Create Legacy User' : null)", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " body.name === pm.collectionVariables.get('legacyOrgName') && body.policies && body.policies.id_quota === 500 && !body.long_name", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "url": "{{baseUrl}}/api/org/{{legacyOrgShortName}}" + } + }, + { + "name": "Create Legacy User", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (body.created && body.created.username === pm.collectionVariables.get('legacyUsername') && body.created.org_UUID && !body.created.status)", + "postman.setNextRequest(ok ? 'Get Legacy User' : null)", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " body.created && body.created.username === pm.collectionVariables.get('legacyUsername') && body.created.org_UUID && !body.created.status", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "url": "{{baseUrl}}/api/org/{{legacyOrgShortName}}/user", + "body": { + "mode": "raw", + "raw": "{\"username\":\"{{legacyUsername}}\",\"name\":{\"first\":\"Legacy\",\"last\":\"User\"},\"authority\":{\"active_roles\":[\"ADMIN\"]}}" + } + } + }, + { + "name": "Get Legacy User", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (body.username === pm.collectionVariables.get('legacyUsername') && body.org_UUID && !body.status && !body.secret)", + "postman.setNextRequest(ok ? 'List Legacy Users' : null)", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " body.username === pm.collectionVariables.get('legacyUsername') && body.org_UUID && !body.status && !body.secret", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "url": "{{baseUrl}}/api/org/{{legacyOrgShortName}}/user/{{legacyUsernameEncoded}}" + } + }, + { + "name": "List Legacy Users", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (Array.isArray(body.users) && body.users.some((x) => x.username === pm.collectionVariables.get('legacyUsername') && x.org_UUID && !x.status))", + "postman.setNextRequest(ok ? 'Get Legacy Org Quota' : null)", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " Array.isArray(body.users) && body.users.some((x) => x.username === pm.collectionVariables.get('legacyUsername') && x.org_UUID && !x.status)", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "url": "{{baseUrl}}/api/org/{{legacyOrgShortName}}/users" + } + }, + { + "name": "Get Legacy Org Quota", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (body.id_quota === 500)", + "postman.setNextRequest(ok ? 'Update Legacy Org' : null)", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " body.id_quota === 500", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "url": "{{baseUrl}}/api/org/{{legacyOrgShortName}}/id_quota" + } + }, + { + "name": "Update Legacy Org", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (body.updated && body.updated.name && body.updated.policies && body.updated.policies.id_quota === 500 && !body.updated.long_name)", + "postman.setNextRequest(ok ? 'Update Legacy User' : null)", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " body.updated && body.updated.name && body.updated.policies && body.updated.policies.id_quota === 500 && !body.updated.long_name", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "url": "{{baseUrl}}/api/org/{{legacyOrgShortName}}?name={{legacyOrgName}}" + } + }, + { + "name": "Update Legacy User", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (body.updated && body.updated.name && body.updated.name.first === 'LegacyUpdated' && !body.updated.status)", + "postman.setNextRequest(ok ? 'Reset Legacy User Secret' : null)", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " body.updated && body.updated.name && body.updated.name.first === 'LegacyUpdated' && !body.updated.status", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "url": "{{baseUrl}}/api/org/{{legacyOrgShortName}}/user/{{legacyUsernameEncoded}}?name.first=LegacyUpdated" + } + }, + { + "name": "Reset Legacy User Secret", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (typeof body['API-secret'] === 'string' && body['API-secret'].length > 0)", + "postman.setNextRequest(ok ? 'Authenticate Legacy User with Reset Secret' : null)", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " typeof body['API-secret'] === 'string' && body['API-secret'].length > 0", + "if (ok) pm.collectionVariables.set('legacyUserApiKey', body['API-secret'])", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "url": "{{baseUrl}}/api/org/{{legacyOrgShortName}}/user/{{legacyUsernameEncoded}}/reset_secret" + } + }, + { + "name": "Authenticate Legacy User with Reset Secret", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "try { body = pm.response.json() } catch (error) {}", + "const ok = pm.response.code === 200 && (body.username === pm.collectionVariables.get('legacyUsername') && !body.status)", + "pm.collectionVariables.set('legacyFlowCompleted', 'true')", + "postman.setNextRequest(pm.collectionVariables.get('registryFlowCompleted') ? null : 'Create Registry Org')", + "pm.test('legacy format contract', () => {", + " pm.expect(ok).to.equal(true)", + " body.username === pm.collectionVariables.get('legacyUsername') && !body.status", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "CVE-API-USER", + "value": "{{legacyUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{legacyOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{legacyUserApiKey}}" + } + ], + "url": "{{baseUrl}}/api/org/{{legacyOrgShortName}}/user/{{legacyUsernameEncoded}}" + } + } + ] + } + ], + "variable": [ + { + "key": "registryRunId", + "value": "" + }, + { + "key": "registryOrgShortName", + "value": "" + }, + { + "key": "registryOrgLongName", + "value": "" + }, + { + "key": "registryOrgUuid", + "value": "" + }, + { + "key": "registryAdminUsername", + "value": "" + }, + { + "key": "registryAdminUsernameEncoded", + "value": "" + }, + { + "key": "registryAdminOrg", + "value": "" + }, + { + "key": "registryAdminApiKey", + "value": "" + }, + { + "key": "registryAdminUserUuid", + "value": "" + }, + { + "key": "registryUserUsername", + "value": "" + }, + { + "key": "registryUserUsernameEncoded", + "value": "" + }, + { + "key": "registryAdditionalUserUsername", + "value": "" + }, + { + "key": "registryUserUuid", + "value": "" + }, + { + "key": "registryUserApiKey", + "value": "" + }, + { + "key": "registryUserOriginalApiKey", + "value": "" + }, + { + "key": "registryAdminUpdateWebsite", + "value": "" + }, + { + "key": "registryCveYear", + "value": "2023" + }, + { + "key": "registryReservedCveId", + "value": "" + } + ] +} diff --git a/src/scripts/test_data/postman/cve-services.postman_environment.json b/src/scripts/test_data/postman/cve-services.postman_environment.json new file mode 100644 index 000000000..edc0dc48e --- /dev/null +++ b/src/scripts/test_data/postman/cve-services.postman_environment.json @@ -0,0 +1,30 @@ +{ + "name": "CVE Services Registry", + "values": [ + { + "key": "baseUrl", + "value": "http://localhost:3000", + "type": "default", + "enabled": true + }, + { + "key": "apiUser", + "value": "test_secretariat_0@mitre.org", + "type": "default", + "enabled": true + }, + { + "key": "apiOrg", + "value": "mitre", + "type": "default", + "enabled": true + }, + { + "key": "apiKey", + "value": "", + "type": "secret", + "enabled": true + } + ], + "_postman_variable_scope": "environment" +} diff --git a/src/swagger.js b/src/swagger.js index 6f491125f..5edc22173 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -6,8 +6,7 @@ const endpointsFiles = [ 'src/controller/org.controller/index.js', 'src/controller/user.controller/index.js', 'src/controller/system.controller/index.js', - 'src/controller/registry-org.controller/index.js', - 'src/controller/registry-user.controller/index.js', + 'src/controller/registry.controller/index.js', 'src/controller/conversation.controller/index.js', 'src/controller/review-object.controller/index.js' ] @@ -22,7 +21,7 @@ const fullCnaContainerRequest = require('../schemas/cve/create-cve-record-cna-re /* eslint-disable no-multi-str */ const doc = { info: { - version: '2.8.3', + version: '2.8.4', title: 'CVE Services API', description: "The CVE Services API supports automation tooling for the CVE Program. Credentials are \ required for most service endpoints. Representatives of \ @@ -479,15 +478,6 @@ const doc = { minimum: 1 } }, - registry: { - in: 'query', - name: 'registry', - description: 'When set to true, the endpoint will expect request data to conform to the applicable User Registry schema, and will provide response data conforming to the applicable User Registry schema. Defaults to false.', - required: false, - schema: { - type: 'boolean' - } - }, short_name: { in: 'query', name: 'short_name', diff --git a/src/utils/authContext.js b/src/utils/authContext.js index 17d5f2799..b67a0ee3a 100644 --- a/src/utils/authContext.js +++ b/src/utils/authContext.js @@ -40,8 +40,8 @@ function isBaseUserRepository (userRepo) { typeof userRepo?.findOneByUsernameAndOrgShortname === 'function' } -function hasOwnMethod (obj, methodName) { - return Object.prototype.hasOwnProperty.call(obj || {}, methodName) && typeof obj[methodName] === 'function' +function hasMethod (obj, methodName) { + return obj != null && typeof obj[methodName] === 'function' } function isAuthenticatedRequest (req) { @@ -76,13 +76,13 @@ async function findOrgByShortName (orgRepo, orgShortName, options = {}, returnLe return orgRepo.findOneByShortName(orgShortName, options) } -async function findUserByUUID (userRepo, userUUID, options = {}, isRegistryObject = true) { +async function findUserByUUID (userRepo, userUUID, options = {}, isLegacyObject = false) { if (!userUUID) { return null } if (typeof userRepo?.findUserByUUID === 'function') { - return userRepo.findUserByUUID(userUUID, options, isRegistryObject) + return userRepo.findUserByUUID(userUUID, options, isLegacyObject) } if (typeof userRepo?.findOneByUUID === 'function') { @@ -92,18 +92,18 @@ async function findUserByUUID (userRepo, userUUID, options = {}, isRegistryObjec return null } -async function findUserByUsernameAndOrgUUID (userRepo, username, orgUUID, options = {}, isRegistryObject = true) { +async function findUserByUsernameAndOrgUUID (userRepo, username, orgUUID, options = {}, isLegacyObject = false) { if (!username || !orgUUID) { return null } if (typeof userRepo?.findUserByUsernameAndOrgUUID === 'function') { - return userRepo.findUserByUsernameAndOrgUUID(username, orgUUID, options, isRegistryObject) + return userRepo.findUserByUsernameAndOrgUUID(username, orgUUID, options, isLegacyObject) } if (typeof userRepo?.findOneByUserNameAndOrgUUID === 'function') { if (isBaseUserRepository(userRepo)) { - return userRepo.findOneByUserNameAndOrgUUID(username, orgUUID, options, isRegistryObject) + return userRepo.findOneByUserNameAndOrgUUID(username, orgUUID, options, isLegacyObject) } return userRepo.findOneByUserNameAndOrgUUID(username, orgUUID, null, options) @@ -125,16 +125,16 @@ async function orgHasRoleByUUID (orgRepo, orgUUID, role, options = {}, returnLeg return orgHasRole(org, role) } -async function isUserAdminOfOrgUUID (userRepo, orgRepo, userUUID, orgUUID, options = {}, isRegistryObject = true) { +async function isUserAdminOfOrgUUID (userRepo, orgRepo, userUUID, orgUUID, options = {}, isLegacyObject = false) { if (!userUUID || !orgUUID) { return false } if (typeof userRepo?.isUserAdminOfOrgUUID === 'function') { - return userRepo.isUserAdminOfOrgUUID(userUUID, orgUUID, options, isRegistryObject) + return userRepo.isUserAdminOfOrgUUID(userUUID, orgUUID, options, isLegacyObject) } - const org = await findOrgByUUID(orgRepo, orgUUID, options, !isRegistryObject) + const org = await findOrgByUUID(orgRepo, orgUUID, options, isLegacyObject) if (!org) { return false } @@ -147,7 +147,7 @@ async function isUserAdminOfOrgUUID (userRepo, orgRepo, userUUID, orgUUID, optio return false } - const user = await findUserByUUID(userRepo, userUUID, options, isRegistryObject) + const user = await findUserByUUID(userRepo, userUUID, options, isLegacyObject) return userHasAdminRole(user) } @@ -188,33 +188,33 @@ async function getRequesterOrg (req, orgRepo, options = {}, returnLegacyFormat = return null } -async function getRequesterUser (req, userRepo, orgRepo, options = {}, isRegistryObject = true) { +async function getRequesterUser (req, userRepo, orgRepo, options = {}, isLegacyObject = false) { if (req.ctx.userUUID) { - return findUserByUUID(userRepo, req.ctx.userUUID, options, isRegistryObject) + return findUserByUUID(userRepo, req.ctx.userUUID, options, isLegacyObject) } if (isAuthenticatedRequest(req) || isUnauthenticatedAfterAuthenticationCheck(req)) { return null } - const orgUUID = await getRequesterOrgUUID(req, orgRepo, options, !isRegistryObject) + const orgUUID = await getRequesterOrgUUID(req, orgRepo, options, isLegacyObject) if (!req.ctx.user || !orgUUID) { return null } - const user = await findUserByUsernameAndOrgUUID(userRepo, req.ctx.user, orgUUID, options, isRegistryObject) + const user = await findUserByUsernameAndOrgUUID(userRepo, req.ctx.user, orgUUID, options, isLegacyObject) if (user) { return user } if (typeof userRepo?.findOneByUsernameAndOrgShortname === 'function') { - return userRepo.findOneByUsernameAndOrgShortname(req.ctx.user, req.ctx.org, options, isRegistryObject) + return userRepo.findOneByUsernameAndOrgShortname(req.ctx.user, req.ctx.org, options, isLegacyObject) } return null } -async function getRequesterUserUUID (req, userRepo, orgRepo, options = {}, isRegistryObject = true) { +async function getRequesterUserUUID (req, userRepo, orgRepo, options = {}, isLegacyObject = false) { if (req.ctx.userUUID) { return req.ctx.userUUID } @@ -225,24 +225,24 @@ async function getRequesterUserUUID (req, userRepo, orgRepo, options = {}, isReg if (!req.ctx.orgUUID && typeof userRepo?.getUserUUID === 'function') { if (isBaseUserRepository(userRepo)) { - return userRepo.getUserUUID(req.ctx.user, req.ctx.org, options, isRegistryObject) + return userRepo.getUserUUID(req.ctx.user, req.ctx.org, options, isLegacyObject) } - const orgUUID = await getRequesterOrgUUID(req, orgRepo, options, !isRegistryObject) + const orgUUID = await getRequesterOrgUUID(req, orgRepo, options, isLegacyObject) return userRepo.getUserUUID(req.ctx.user, orgUUID, options) } - const user = await getRequesterUser(req, userRepo, orgRepo, options, isRegistryObject) + const user = await getRequesterUser(req, userRepo, orgRepo, options, isLegacyObject) if (user?.UUID) { return user.UUID } if (typeof userRepo?.getUserUUID === 'function') { if (isBaseUserRepository(userRepo)) { - return userRepo.getUserUUID(req.ctx.user, req.ctx.org, options, isRegistryObject) + return userRepo.getUserUUID(req.ctx.user, req.ctx.org, options, isLegacyObject) } - const orgUUID = await getRequesterOrgUUID(req, orgRepo, options, !isRegistryObject) + const orgUUID = await getRequesterOrgUUID(req, orgRepo, options, isLegacyObject) return userRepo.getUserUUID(req.ctx.user, orgUUID, options) } @@ -289,7 +289,7 @@ async function isRequesterSecretariat (req, orgRepo, options = {}, returnLegacyF return false } - if (hasOwnMethod(orgRepo, 'isSecretariatByShortName')) { + if (hasMethod(orgRepo, 'isSecretariatByShortName')) { return orgRepo.isSecretariatByShortName(req.ctx.org, options, returnLegacyFormat) } @@ -335,13 +335,13 @@ async function isRequesterBulkDownload (req, orgRepo, options = {}, returnLegacy return false } -async function isRequesterAdmin (req, userRepo, orgRepo, options = {}, isRegistryObject = true) { +async function isRequesterAdmin (req, userRepo, orgRepo, options = {}, isLegacyObject = false) { if (isAuthenticatedRequest(req) || (req.ctx.orgUUID && req.ctx.userUUID)) { if (!req.ctx.orgUUID || !req.ctx.userUUID) { return false } - return isUserAdminOfOrgUUID(userRepo, orgRepo, req.ctx.userUUID, req.ctx.orgUUID, options, isRegistryObject) + return isUserAdminOfOrgUUID(userRepo, orgRepo, req.ctx.userUUID, req.ctx.orgUUID, options, isLegacyObject) } if (isUnauthenticatedAfterAuthenticationCheck(req)) { @@ -349,13 +349,13 @@ async function isRequesterAdmin (req, userRepo, orgRepo, options = {}, isRegistr } if (typeof userRepo?.isAdmin === 'function') { - return userRepo.isAdmin(req.ctx.user, req.ctx.org, options, isRegistryObject) + return userRepo.isAdmin(req.ctx.user, req.ctx.org, options, isLegacyObject) } return false } -async function isRequesterAdminOfOrg (req, userRepo, orgRepo, targetOrgOrShortName, options = {}, isRegistryObject = true) { +async function isRequesterAdminOfOrg (req, userRepo, orgRepo, targetOrgOrShortName, options = {}, isLegacyObject = false) { const fallbackTargetShortName = typeof targetOrgOrShortName === 'string' ? targetOrgOrShortName : targetOrgOrShortName?.short_name @@ -369,26 +369,26 @@ async function isRequesterAdminOfOrg (req, userRepo, orgRepo, targetOrgOrShortNa let targetOrgUUID = targetOrg?.UUID || null if (targetOrgUUID && !Array.isArray(targetOrg.admins)) { - const fullTargetOrg = await findOrgByUUID(orgRepo, targetOrgUUID, options, !isRegistryObject) + const fullTargetOrg = await findOrgByUUID(orgRepo, targetOrgUUID, options, isLegacyObject) targetOrg = fullTargetOrg || targetOrg } if (!targetOrg && fallbackTargetShortName) { - targetOrg = await findOrgByShortName(orgRepo, fallbackTargetShortName, options, !isRegistryObject) + targetOrg = await findOrgByShortName(orgRepo, fallbackTargetShortName, options, isLegacyObject) } targetOrgUUID = targetOrg?.UUID || targetOrgUUID if (!targetOrgUUID && fallbackTargetShortName) { - targetOrgUUID = await getTargetOrgUUID(orgRepo, fallbackTargetShortName, options, !isRegistryObject) + targetOrgUUID = await getTargetOrgUUID(orgRepo, fallbackTargetShortName, options, isLegacyObject) } - if (await isUserAdminOfOrgUUID(userRepo, orgRepo, req.ctx.userUUID, targetOrgUUID, options, isRegistryObject)) { + if (await isUserAdminOfOrgUUID(userRepo, orgRepo, req.ctx.userUUID, targetOrgUUID, options, isLegacyObject)) { return true } const sameOrg = Boolean(req.ctx.orgUUID && targetOrgUUID && req.ctx.orgUUID === targetOrgUUID) if (sameOrg) { - const user = await getRequesterUser(req, userRepo, orgRepo, options, isRegistryObject) + const user = await getRequesterUser(req, userRepo, orgRepo, options, isLegacyObject) return userHasAdminRole(user) } @@ -400,30 +400,30 @@ async function isRequesterAdminOfOrg (req, userRepo, orgRepo, targetOrgOrShortNa } if (!req.ctx.orgUUID && !req.ctx.userUUID) { - if (hasOwnMethod(userRepo, 'isAdminOrSecretariat')) { - return userRepo.isAdminOrSecretariat(fallbackTargetShortName, req.ctx.user, req.ctx.org, options, isRegistryObject) + if (hasMethod(userRepo, 'isAdminOrSecretariat')) { + return userRepo.isAdminOrSecretariat(fallbackTargetShortName, req.ctx.user, req.ctx.org, options, isLegacyObject) } - if (hasOwnMethod(userRepo, 'isAdmin')) { - return userRepo.isAdmin(req.ctx.user, fallbackTargetShortName, options, isRegistryObject) + if (hasMethod(userRepo, 'isAdmin')) { + return userRepo.isAdmin(req.ctx.user, fallbackTargetShortName, options, isLegacyObject) } } let targetOrg = typeof targetOrgOrShortName === 'string' ? null : targetOrgOrShortName if (!targetOrg && fallbackTargetShortName) { - targetOrg = await findOrgByShortName(orgRepo, fallbackTargetShortName, options, !isRegistryObject) + targetOrg = await findOrgByShortName(orgRepo, fallbackTargetShortName, options, isLegacyObject) } if (req.ctx.userUUID) { - const targetOrgUUID = targetOrg?.UUID || (fallbackTargetShortName ? await getTargetOrgUUID(orgRepo, fallbackTargetShortName, options, !isRegistryObject) : null) - if (await isUserAdminOfOrgUUID(userRepo, orgRepo, req.ctx.userUUID, targetOrgUUID, options, isRegistryObject)) { + const targetOrgUUID = targetOrg?.UUID || (fallbackTargetShortName ? await getTargetOrgUUID(orgRepo, fallbackTargetShortName, options, isLegacyObject) : null) + if (await isUserAdminOfOrgUUID(userRepo, orgRepo, req.ctx.userUUID, targetOrgUUID, options, isLegacyObject)) { return true } - const sameOrg = await isRequesterSameOrg(req, orgRepo, targetOrg || targetOrgOrShortName, options, !isRegistryObject) + const sameOrg = await isRequesterSameOrg(req, orgRepo, targetOrg || targetOrgOrShortName, options, isLegacyObject) if (sameOrg) { - const user = await getRequesterUser(req, userRepo, orgRepo, options, isRegistryObject) + const user = await getRequesterUser(req, userRepo, orgRepo, options, isLegacyObject) return userHasAdminRole(user) } @@ -431,13 +431,13 @@ async function isRequesterAdminOfOrg (req, userRepo, orgRepo, targetOrgOrShortNa } if (typeof userRepo?.isAdmin === 'function') { - return userRepo.isAdmin(req.ctx.user, fallbackTargetShortName, options, isRegistryObject) + return userRepo.isAdmin(req.ctx.user, fallbackTargetShortName, options, isLegacyObject) } return false } -async function getRequesterContext (req, repositories = {}, options = {}, isRegistryObject = true) { +async function getRequesterContext (req, repositories = {}, options = {}, isLegacyObject = false) { const orgRepo = repositories.orgRepo const userRepo = repositories.userRepo const context = { @@ -451,16 +451,16 @@ async function getRequesterContext (req, repositories = {}, options = {}, isRegi } if (orgRepo) { - context.orgUUID = await getRequesterOrgUUID(req, orgRepo, options, !isRegistryObject) - context.org = await getRequesterOrg(req, orgRepo, options, !isRegistryObject) - context.isSecretariat = await isRequesterSecretariat(req, orgRepo, options, !isRegistryObject) - context.isBulkDownload = await isRequesterBulkDownload(req, orgRepo, options, !isRegistryObject) + context.orgUUID = await getRequesterOrgUUID(req, orgRepo, options, isLegacyObject) + context.org = await getRequesterOrg(req, orgRepo, options, isLegacyObject) + context.isSecretariat = await isRequesterSecretariat(req, orgRepo, options, isLegacyObject) + context.isBulkDownload = await isRequesterBulkDownload(req, orgRepo, options, isLegacyObject) } if (userRepo && orgRepo) { - context.userUUID = await getRequesterUserUUID(req, userRepo, orgRepo, options, isRegistryObject) - context.user = await getRequesterUser(req, userRepo, orgRepo, options, isRegistryObject) - context.isAdmin = await isRequesterAdmin(req, userRepo, orgRepo, options, isRegistryObject) + context.userUUID = await getRequesterUserUUID(req, userRepo, orgRepo, options, isLegacyObject) + context.user = await getRequesterUser(req, userRepo, orgRepo, options, isLegacyObject) + context.isAdmin = await isRequesterAdmin(req, userRepo, orgRepo, options, isLegacyObject) } return context diff --git a/src/utils/db.js b/src/utils/db.js index 4b3be05a8..6cfb6ec84 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -7,6 +7,12 @@ const logger = require('../middleware/logger') */ function getMongoConnectionString () { const appEnv = process.env.NODE_ENV + if (process.env.MONGO_CONN_STRING && process.env.NODE_ENV !== 'production') { + logger.info(`Using NODE_ENV '${process.env.NODE_ENV}' and app environment '${appEnv}'`) + logger.info('Using MONGO_CONN_STRING override') + return process.env.MONGO_CONN_STRING + } + let dbUser, dbPassword if (process.env.MONGO_USER && process.env.MONGO_PASSWORD) { dbUser = process.env.MONGO_USER diff --git a/test/integration-tests/controller/sessionOptionsTest.js b/test/integration-tests/controller/sessionOptionsTest.js new file mode 100644 index 000000000..4a8e3cb63 --- /dev/null +++ b/test/integration-tests/controller/sessionOptionsTest.js @@ -0,0 +1,45 @@ +/* global describe, it */ + +const fs = require('fs') +const path = require('path') +const { expect } = require('chai') + +function getControllerFiles (dir) { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + return getControllerFiles(fullPath) + } + + if (entry.isFile() && entry.name.endsWith('.js')) { + return [fullPath] + } + + return [] + }) +} + +function lineNumberForIndex (content, index) { + return content.slice(0, index).split('\n').length +} + +describe('Controller mongoose session options', () => { + it('starts all mongoose sessions with causal consistency disabled', () => { + const controllerRoot = path.join(__dirname, '../../../src/controller') + const missingOptions = [] + + for (const file of getControllerFiles(controllerRoot)) { + const content = fs.readFileSync(file, 'utf8') + const startSessionCalls = content.matchAll(/mongoose\.startSession\(([^)]*)\)/gs) + + for (const match of startSessionCalls) { + if (!/causalConsistency\s*:\s*false/.test(match[1])) { + missingOptions.push(`${path.relative(process.cwd(), file)}:${lineNumberForIndex(content, match.index)}`) + } + } + } + + expect(missingOptions).to.deep.equal([]) + }) +}) diff --git a/test/integration-tests/org/regularUsersTestRegistry.js b/test/integration-tests/org/regularUsersTestRegistry.js index bdcb58b80..252122f48 100644 --- a/test/integration-tests/org/regularUsersTestRegistry.js +++ b/test/integration-tests/org/regularUsersTestRegistry.js @@ -318,16 +318,6 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with expect(res).to.have.status(403) expect(res.body.error).to.contain('NOT_SAME_USER_OR_SECRETARIAT') }) - /* Commenting out since authority.active_roles are not returned in the GET request response for registry=true */ - // await chai.request(app) - // .get(`/api/org/${org}/user/${user}?registry=true`) - // .set(constants.nonSecretariatUserHeaders2) - // .send({ - // }) - // .then((res) => { - // expect(res).to.have.status(200) - // console.log(res.body) - // }) }) }) }) diff --git a/test/integration-tests/registry-org/createUserByOrgTest.js b/test/integration-tests/registry-org/createUserByOrgTest.js index 14f3a64cf..be9a94d9c 100644 --- a/test/integration-tests/registry-org/createUserByOrgTest.js +++ b/test/integration-tests/registry-org/createUserByOrgTest.js @@ -8,7 +8,7 @@ const expect = chai.expect const constants = require('../constants.js') const app = require('../../../src/index.js') -describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { +describe('Testing POST /api/registry/org/:shortname/user endpoint', () => { context('Positive Tests', () => { it('Should create a new user in an organization', (done) => { const orgShortName = 'mitre' @@ -22,7 +22,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { role: 'ADMIN' } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(newUser) .end((err, res) => { @@ -48,7 +48,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { status: 'active' } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(newUser) .end((err, res) => { @@ -69,7 +69,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { status: 'active' } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(existingUser) .end((err, res) => { @@ -89,7 +89,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { } } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(invalidUser) .end((err, res) => { @@ -111,7 +111,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { test: 'additional key not in schema' } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(existingUser) .then((res) => { diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 6908d400c..8b0cf5320 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -36,8 +36,8 @@ function expectConversationWithoutAuthorId (conversations, body) { expect(convo).to.not.have.property('author_id') } -describe('Testing /registryOrg endpoints', () => { - context('Testing POST /registryOrg endpoint', () => { +describe('Testing /registry/org endpoints', () => { + context('Testing POST /registry/org endpoint', () => { context('Positive Tests', () => { it('Creates a new registry org', async () => { await chai.request(app) @@ -154,6 +154,36 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.program_data.cve_website_update_date).to.equal(cveWebsiteUpdateDate) }) }) + it('Creates distinct registry orgs when a short name contains regex metacharacters', async () => { + const plainShortName = 'regexxdot_org_test' + const regexShortName = 'regex.dot_org_test' + + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + short_name: plainShortName, + long_name: 'Regex X Dot Org Test' + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.created.short_name).to.equal(plainShortName) + }) + + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + short_name: regexShortName, + long_name: 'Regex Dot Org Test' + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.created.short_name).to.equal(regexShortName) + }) + }) }) context('Negative Tests', () => { it('Fails to create a new registry organization with an existing short name', async () => { @@ -310,7 +340,7 @@ describe('Testing /registryOrg endpoints', () => { }) }) }) - context('Testing GET /registryOrg endpoints', () => { + context('Testing GET /registry/org endpoints', () => { context('Positive Tests', () => { it('Gets a list of all registry organizations', async () => { await chai.request(app) @@ -629,7 +659,7 @@ describe('Testing /registryOrg endpoints', () => { }) }) }) - context('Testing PUT /registryOrg endpoint', () => { + context('Testing PUT /registry/org endpoint', () => { context('Positive Tests', () => { it('Updates a registry organization providing a full organization object', async () => { await chai.request(app) @@ -841,7 +871,7 @@ describe('Testing /registryOrg endpoints', () => { .delete('/api/registry/org/temp_org_updated_name') .set(secretariatHeaders) }) - it('Updates a registry organization to oversee another, and verifies the sub-org dynamically returns reports_to', async () => { + it('Does not update oversees through the standard registry organization PUT endpoint', async () => { // Create a sub org const subOrg = { short_name: 'sub_org_test', @@ -859,7 +889,7 @@ describe('Testing /registryOrg endpoints', () => { createdSubOrgUUID = res.body.created.UUID }) - // Update the main org to oversee it + // A standard PUT must ignore oversees; reports-to relationships use dedicated endpoints. await chai.request(app) .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) @@ -869,41 +899,25 @@ describe('Testing /registryOrg endpoints', () => { }) .then(res => { expect(res).to.have.status(200) - expect(res.body.updated.oversees).to.be.an('array').that.includes(createdSubOrgUUID) + expect(res.body.updated.oversees || []).to.not.include(createdSubOrgUUID) }) const BaseOrg = require('../../../src/model/baseorg') const registryOrgCheck = await BaseOrg.findOne({ short_name: createdOrg.short_name }) - expect(registryOrgCheck.oversees).to.be.an('array').that.includes(createdSubOrgUUID) + expect(registryOrgCheck.oversees || []).to.not.include(createdSubOrgUUID) - // Assert that the sub org dynamically returns reports_to matching the main org's UUID + // The reporting org must not acquire a computed reports_to relationship. await chai.request(app) .get(`/api/registry/org/${subOrg.short_name}`) .set(secretariatHeaders) .then(res => { expect(res).to.have.status(200) - expect(res.body).to.have.property('reports_to', createdOrg.UUID) - expect(res.body).to.have.property('_relatedOrganizations') - expect(res.body._relatedOrganizations).to.be.an('array').that.has.lengthOf(1) - expect(res.body._relatedOrganizations[0].UUID).to.equal(createdOrg.UUID) - expect(res.body._relatedOrganizations[0].short_name).to.equal(createdOrg.short_name) - }) - - // Assert that the main org also has _relatedOrganizations for the sub org it oversees - await chai.request(app) - .get(`/api/registry/org/${createdOrg.short_name}`) - .set(secretariatHeaders) - .then(res => { - expect(res).to.have.status(200) - expect(res.body).to.have.property('_relatedOrganizations') - expect(res.body._relatedOrganizations).to.be.an('array').that.has.lengthOf(1) - expect(res.body._relatedOrganizations[0].UUID).to.equal(createdSubOrgUUID) - expect(res.body._relatedOrganizations[0].short_name).to.equal(subOrg.short_name) + expect(res.body).to.not.have.property('reports_to') }) // Cleanup sub org await chai.request(app) - .delete(`/api/registryOrg/${subOrg.short_name}`) + .delete(`/api/registry/org/${subOrg.short_name}`) .set(secretariatHeaders) }) it('Preserves inUse and in_use properties across updates', async () => { @@ -945,7 +959,7 @@ describe('Testing /registryOrg endpoints', () => { // Cleanup await chai.request(app) - .delete(`/api/registryOrg/${tempOrg.short_name}`) + .delete(`/api/registry/org/${tempOrg.short_name}`) .set(secretariatHeaders) }) }) @@ -1122,11 +1136,11 @@ describe('Testing /registryOrg endpoints', () => { }) }) }) - context('Testing DELETE /registryOrg endpoint', () => { + context('Testing DELETE /registry/org endpoint', () => { context('Positive Tests', () => { it('Deletes a registry organization with the provided short name', async () => { await chai.request(app) - .delete('/api/registryOrg/registry_org_test') + .delete('/api/registry/org/registry_org_test') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -1137,7 +1151,7 @@ describe('Testing /registryOrg endpoints', () => { context('Negative Tests', () => { it('Fails to delete a registry organization that does not exist', async () => { await chai.request(app) - .delete('/api/registryOrg/registry_org_test2') + .delete('/api/registry/org/registry_org_test2') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) diff --git a/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js b/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js index 7fa072f43..ed3ea8b70 100644 --- a/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js +++ b/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js @@ -16,7 +16,7 @@ describe('Testing Registry Org Discriminator Authority inheritance', () => { const shortName = orgsToCleanup.pop() try { await chai.request(app) - .delete(`/api/registryOrg/${shortName}`) + .delete(`/api/registry/org/${shortName}`) .set(secretariatHeaders) } catch (err) { // ignore errors during cleanup diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js index d891e91c4..28cd37220 100644 --- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -197,7 +197,7 @@ describe('Testing Joint approval', () => { }) it('Check to see if the org was partially updated', async () => { await chai.request(app) - .get(`/api/registryOrg/${orgUUID}`) + .get(`/api/registry/org/${orgUUID}`) .set(secretariatHeaders) .then((res, err) => { expect(err).to.be.undefined @@ -217,7 +217,7 @@ describe('Testing Joint approval', () => { }) // Verify that the org was updated with the new body values await chai.request(app) - .get(`/api/registryOrg/${orgUUID}`) + .get(`/api/registry/org/${orgUUID}`) .set(secretariatHeaders) .then((res, err) => { expect(err).to.be.undefined @@ -233,7 +233,7 @@ describe('Testing Joint approval', () => { let reviewUUID it('Create an org to use for testing', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(testRegistryOrgForReviewWithComments) .then((res, err) => { @@ -300,7 +300,7 @@ describe('Testing Joint approval', () => { }) it('Check to see if the org was partially updated', async () => { await chai.request(app) - .get(`/api/registryOrg/${orgUUID}`) + .get(`/api/registry/org/${orgUUID}`) .set(secretariatHeaders) .then((res, err) => { expect(err).to.be.undefined @@ -373,7 +373,7 @@ describe('Testing Joint approval', () => { }) it('Check to see if the org was fully updated', async () => { await chai.request(app) - .get(`/api/registryOrg/${orgUUID}`) + .get(`/api/registry/org/${orgUUID}`) .set(secretariatHeaders) .then((res, err) => { expect(err).to.be.undefined diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index 689a0af81..18c4bd7e1 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -16,6 +16,8 @@ const testRootOrg = { authority: ['ROOT'] } let createdOrg +let reportingOrg +let secondRootOrg describe('Testing ROOT Organization Type', () => { context('Creating a ROOT org', () => { @@ -55,6 +57,134 @@ describe('Testing ROOT Organization Type', () => { }) }) + context('Managing reports-to relationships', () => { + before(async () => { + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + short_name: 'reporting_org_for_root', + long_name: 'Reporting Organization', + authority: ['CNA'], + id_quota: 100 + }) + .then((res) => { + expect(res).to.have.status(200) + reportingOrg = res.body.created + }) + + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + short_name: 'second_root_org_test', + long_name: 'Second Root Organization', + authority: ['ROOT'] + }) + .then((res) => { + expect(res).to.have.status(200) + secondRootOrg = res.body.created + }) + }) + + it('Secretariat can assign an organization to report to a ROOT organization', async () => { + await chai.request(app) + .post(`/api/registry/org/${testRootOrg.short_name}/oversees/${reportingOrg.short_name}/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.equal(`${reportingOrg.short_name} organization now reports to ${testRootOrg.short_name}.`) + }) + + await chai.request(app) + .get(`/api/registry/org/${reportingOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.reports_to).to.equal(createdOrg.UUID) + }) + + await chai.request(app) + .get(`/api/registry/org/${testRootOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.oversees).to.include(reportingOrg.UUID) + }) + }) + + it('Assigning a new ROOT parent removes the previous reports-to relationship', async () => { + await chai.request(app) + .post(`/api/registry/org/${secondRootOrg.short_name}/oversees/${reportingOrg.short_name}/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + }) + + await chai.request(app) + .get(`/api/registry/org/${testRootOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.oversees || []).to.not.include(reportingOrg.UUID) + }) + + await chai.request(app) + .get(`/api/registry/org/${reportingOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.reports_to).to.equal(secondRootOrg.UUID) + }) + }) + + it('Secretariat can remove a reports-to relationship', async () => { + await chai.request(app) + .post(`/api/registry/org/${secondRootOrg.short_name}/oversees/${reportingOrg.short_name}/remove`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.equal(`${reportingOrg.short_name} organization no longer reports to ${secondRootOrg.short_name}.`) + }) + + await chai.request(app) + .get(`/api/registry/org/${reportingOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.not.have.property('reports_to') + }) + }) + + it('Rejects a non-ROOT overseeing organization', async () => { + await chai.request(app) + .post(`/api/registry/org/${reportingOrg.short_name}/oversees/${testRootOrg.short_name}/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.error).to.equal('OVERSEEING_ORG_MUST_BE_ROOT') + }) + }) + + it('Returns not found when either organization does not exist', async () => { + await chai.request(app) + .post(`/api/registry/org/missing_root/oversees/${reportingOrg.short_name}/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.error).to.equal('ORG_DNE_PARAM') + }) + + await chai.request(app) + .post(`/api/registry/org/${testRootOrg.short_name}/oversees/missing_reporting_org/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.error).to.equal('ORG_DNE_PARAM') + }) + }) + }) + context('ROOT admin permissions', () => { before(async () => { // Create a Root Admin user @@ -108,17 +238,26 @@ describe('Testing ROOT Organization Type', () => { }) }) - it('ROOT admin cannot edit oversees', async () => { + it('ROOT admin cannot set oversees through the standard PUT endpoint', async () => { await chai.request(app) .put(`/api/registry/org/${testRootOrg.short_name}`) .set(rootAdminHeaders) .send({ ...createdOrg, - oversees: ['some_other_uuid'] + oversees: [reportingOrg.UUID] + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.updated.oversees || []).to.not.include(reportingOrg.UUID) }) + }) + + it('ROOT admin cannot manage reports-to relationships', async () => { + await chai.request(app) + .post(`/api/registry/org/${testRootOrg.short_name}/oversees/${reportingOrg.short_name}/add`) + .set(rootAdminHeaders) .then((res) => { expect(res).to.have.status(403) - expect(res.body.error).to.equal('SECRETARIAT_ONLY') }) }) diff --git a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js index c6abde3a3..6884a6784 100644 --- a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js +++ b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js @@ -22,7 +22,7 @@ describe('Testing Deep Remove Empty in Create Org', () => { context('Positive Tests', () => { it('Creates a registry org and verifies null values are removed', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(testNullRemovalOrg) .then((res, err) => { @@ -56,7 +56,7 @@ describe('Testing Deep Remove Empty in Create Org', () => { after(async () => { // Cleanup: Delete the created org await chai.request(app) - .delete('/api/registryOrg/test_null_removal') + .delete('/api/registry/org/test_null_removal') .set(secretariatHeaders) }) }) diff --git a/test/integration-tests/registry-user/registryUserCRUDTest.js b/test/integration-tests/registry-user/registryUserCRUDTest.js index 01c089c98..b696e01d2 100644 --- a/test/integration-tests/registry-user/registryUserCRUDTest.js +++ b/test/integration-tests/registry-user/registryUserCRUDTest.js @@ -40,7 +40,7 @@ const postNewOrg = async (shortName) => { const postNewUser = async (orgShortName, username) => { return chai.request(app) - .post(`/api/registryUser/${orgShortName}`) + .post(`/api/registry/org/${orgShortName}/user`) .set(secretariatHeaders) .send({ username, @@ -71,11 +71,11 @@ const createRegistryUser = async () => { return { orgShortName, createdUser } } -describe('Testing /registryUser endpoints', () => { +describe('Testing canonical registry user endpoints', () => { context('Positive Tests', () => { it('Gets a list of all registry users', async () => { await chai.request(app) - .get('/api/registryUser') + .get('/api/registry/users') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -84,7 +84,7 @@ describe('Testing /registryUser endpoints', () => { }) }) - it('Gets a registry user by UUID', async () => { + it('Gets a registry user by organization and username', async () => { let user await chai.request(app) .get('/api/registry/org/win_5/user/jasminesmith@win_5.com') @@ -95,7 +95,7 @@ describe('Testing /registryUser endpoints', () => { }) await chai.request(app) - .get(`/api/registryUser/${user.UUID}`) + .get(`/api/registry/org/win_5/user/${user.username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -105,12 +105,11 @@ describe('Testing /registryUser endpoints', () => { }) }) - it('Creates, updates, and deletes a registry user by UUID', async () => { + it('Creates, updates, and deletes a registry user by organization and username', async () => { const username = `${uuidv4()}@registry-user.test` - let userUUID await chai.request(app) - .post('/api/registryUser/range_4') + .post('/api/registry/org/range_4/user') .set(secretariatHeaders) .send({ username, @@ -125,12 +124,11 @@ describe('Testing /registryUser endpoints', () => { expect(res.body).to.have.property('created') expect(res.body.created).to.have.property('UUID') expect(res.body.created).to.have.property('username', username) - userUUID = res.body.created.UUID }) let user await chai.request(app) - .get(`/api/registryUser/${userUUID}`) + .get(`/api/registry/org/range_4/user/${username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -138,7 +136,7 @@ describe('Testing /registryUser endpoints', () => { }) await chai.request(app) - .put(`/api/registryUser/${userUUID}`) + .put(`/api/registry/org/range_4/user/${username}`) .set(secretariatHeaders) .send({ ...user, @@ -153,7 +151,7 @@ describe('Testing /registryUser endpoints', () => { }) await chai.request(app) - .delete(`/api/registryUser/${userUUID}`) + .delete(`/api/registry/org/range_4/user/${username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -161,20 +159,20 @@ describe('Testing /registryUser endpoints', () => { }) await chai.request(app) - .get(`/api/registryUser/${userUUID}`) + .get(`/api/registry/org/range_4/user/${username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) }) }) - it('Logs the updated user UUID when updating a registry user by identifier', async () => { - const { createdUser } = await createRegistryUser() + it('Logs the updated user UUID when updating a registry user by canonical identity', async () => { + const { orgShortName, createdUser } = await createRegistryUser() const loggerInfoStub = sinon.stub(logger, 'info') try { await chai.request(app) - .put(`/api/registryUser/${createdUser.UUID}`) + .put(`/api/registry/org/${orgShortName}/user/${createdUser.username}`) .set(secretariatHeaders) .send({ UUID: createdUser.UUID, @@ -198,13 +196,13 @@ describe('Testing /registryUser endpoints', () => { } }) - it('Logs the deleted user UUID when deleting a registry user by identifier', async () => { - const { createdUser } = await createRegistryUser() + it('Logs the deleted user UUID when deleting a registry user by canonical identity', async () => { + const { orgShortName, createdUser } = await createRegistryUser() const loggerInfoStub = sinon.stub(logger, 'info') try { await chai.request(app) - .delete(`/api/registryUser/${createdUser.UUID}`) + .delete(`/api/registry/org/${orgShortName}/user/${createdUser.username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -221,7 +219,7 @@ describe('Testing /registryUser endpoints', () => { context('Negative Tests', () => { it('Fails when page query parameter is not an integer', async () => { await chai.request(app) - .get('/api/registryUser') + .get('/api/registry/users') .set(secretariatHeaders) // Must be secretariat to reach validation .query({ page: 'not-a-number' }) // Invalid data .then((res) => { @@ -232,7 +230,7 @@ describe('Testing /registryUser endpoints', () => { it('Fails when page query parameter is below the minimum', async () => { await chai.request(app) - .get('/api/registryUser') + .get('/api/registry/users') .set(secretariatHeaders) .query({ page: 0 }) // Assuming min is 1 .then((res) => { @@ -240,9 +238,9 @@ describe('Testing /registryUser endpoints', () => { }) }) - it('Fails when identifier contains invalid characters', async () => { + it('Fails when username contains invalid characters', async () => { await chai.request(app) - .get('/api/registryUser/uuid