diff --git a/api/source/controllers/Asset.js b/api/source/controllers/Asset.js index 0cec12dac..d20dc859b 100644 --- a/api/source/controllers/Asset.js +++ b/api/source/controllers/Asset.js @@ -5,6 +5,7 @@ const config = require('../utils/config') const escape = require('../utils/escape') const AssetService = require(`../service/AssetService`); const CollectionService = require(`../service/CollectionService`); +const Security = require('../utils/accessLevels') const dbUtils = require(`../service/utils`) const {XMLBuilder} = require("fast-xml-parser") const SmError = require('../utils/error') @@ -785,47 +786,33 @@ module.exports.deleteAssetMetadataKey = async function (req, res, next) { } } -module.exports.getAssetLabels = async function (req, res, next) { +module.exports.patchAssets = async function (req, res, next) { try { - res.json({}) + const collectionId = getCollectionIdAndCheckPermission(req, Security.ACCESS_LEVEL.Manage) + const patchRequest = req.body + const collection = await CollectionService.getCollection( collectionId, ['assets'], false, req.userObject) + const collectionAssets = collection.assets.map( a => a.assetId) + if (!patchRequest.assetIds.every( a => collectionAssets.includes(a))) { + throw new SmError.PrivilegeError('One or more assetId is not a Collection member.') + } + await AssetService.deleteAssets(patchRequest.assetIds, req.userObject) + res.json({ + operation: 'deleted', + assetIds: patchRequest.assetIds + }) } catch (err) { next(err) } } -module.exports.addAssetLabels = async function (req, res, next) { - try { - res.json({}) - } - catch (err) { - next(err) +function getCollectionIdAndCheckPermission(request, minimumAccessLevel = Security.ACCESS_LEVEL.Manage) { + let collectionId = request.query.collectionId + const collectionGrant = request.userObject.collectionGrants.find( g => g.collection.collectionId === collectionId ) + if (collectionGrant?.accessLevel < minimumAccessLevel) { + throw new SmError.PrivilegeError() } + return collectionId } -module.exports.replaceAssetLabels = async function (req, res, next) { - try { - res.json({}) - } - catch (err) { - next(err) - } -} -module.exports.deleteAssetLabels = async function (req, res, next) { - try { - res.json({}) - } - catch (err) { - next(err) - } -} - -module.exports.deleteAssetLabelById = async function (req, res, next) { - try { - res.json({}) - } - catch (err) { - next(err) - } -} diff --git a/api/source/service/AssetService.js b/api/source/service/AssetService.js index 89e163efc..c00897aaa 100644 --- a/api/source/service/AssetService.js +++ b/api/source/service/AssetService.js @@ -1333,6 +1333,13 @@ exports.deleteAsset = async function(assetId, projection, elevate, userObject) { return (rows[0]) } +exports.deleteAssets = async function(assetIds, userObject) { + const sqlDelete = `UPDATE asset SET state = "disabled", stateDate = NOW(), stateUserId = ? where assetId IN ?` + await dbUtils.pool.query(sqlDelete, [userObject.userId, [assetIds]]) + // changes above might have affected need for records in collection_rev_map + await dbUtils.pruneCollectionRevMap() + await dbUtils.updateDefaultRev(null, {}) +} exports.attachStigToAsset = async function( {assetId, benchmarkId, collectionId, elevate, userObject, svcStatus = {}} ) { diff --git a/api/source/specification/stig-manager.yaml b/api/source/specification/stig-manager.yaml index 50c9d8cc2..1e083128b 100644 --- a/api/source/specification/stig-manager.yaml +++ b/api/source/specification/stig-manager.yaml @@ -10,8 +10,6 @@ servers: - url: 'http://localhost:64001/api' paths: /assets: - parameters: - - $ref: '#/components/parameters/AssetProjectionQuery' get: tags: - Asset @@ -26,6 +24,7 @@ paths: - $ref: '#/components/parameters/NameMatchQuery' - $ref: '#/components/parameters/MetadataQuery' - $ref: '#/components/parameters/BenchmarkIdQuery' + - $ref: '#/components/parameters/AssetProjectionQuery' responses: '200': description: AssetProjected array response @@ -49,6 +48,8 @@ paths: - Asset summary: Create an Asset operationId: createAsset + parameters: + - $ref: '#/components/parameters/AssetProjectionQuery' requestBody: required: true content: @@ -77,6 +78,42 @@ paths: security: - oauth: - 'stig-manager:collection' + patch: + tags: + - Asset + summary: Delete one or more Assets + operationId: patchAssets + parameters: + - $ref: '#/components/parameters/CollectionIdQuery' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AssetsPatchRequest' + responses: + '200': + description: AssetsPatch response + content: + application/json: + schema: + $ref: '#/components/schemas/AssetsPatchResponse' + '400': + description: Client Error + content: + application/json: + schema: + $ref: '#/components/schemas/ClientErrorDuplicateAsset' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + security: + - oauth: + - 'stig-manager:collection' + '/assets/{assetId}': parameters: - $ref: '#/components/parameters/AssetIdPath' @@ -4031,6 +4068,30 @@ components: type: array items: $ref: '#/components/schemas/StigUserBasic' + AssetsPatchRequest: + type: object + additionalProperties: false + properties: + operation: + type: string + enum: + - delete + assetIds: + type: array + items: + $ref: '#/components/schemas/AssetId' + AssetsPatchResponse: + type: object + additionalProperties: false + properties: + operation: + type: string + enum: + - deleted + assetIds: + type: array + items: + $ref: '#/components/schemas/AssetId' AssetStigResponse: type: object additionalProperties: false diff --git a/client/src/js/SM/CollectionAsset.js b/client/src/js/SM/CollectionAsset.js index 2ffc3f3fe..a03ca0817 100644 --- a/client/src/js/SM/CollectionAsset.js +++ b/client/src/js/SM/CollectionAsset.js @@ -265,24 +265,26 @@ SM.CollectionAssetGrid = Ext.extend(Ext.grid.GridPanel, { try { let assetRecords = me.getSelectionModel().getSelections() const multiDelete = assetRecords.length > 1 - var confirmStr=`Deleting ${multiDelete ? 'multiple assets' : 'this asset'} will permanently remove all data associated with the asset${multiDelete ? 's' : ''}. This includes all the corresponding STIG assessments. The deleted data cannot be recovered.

Do you wish to continue?`; + const confirmStr=`Deleting ${multiDelete ? 'multiple assets' : 'this asset'} will permanently remove all data associated with the asset${multiDelete ? 's' : ''}. This includes all the corresponding STIG assessments. The deleted data cannot be recovered.

Do you wish to continue?`; let btn = await SM.confirmPromise("Confirm Delete", confirmStr) if (btn == 'yes') { - const l = assetRecords.length - let apiAsset - for (let i=0; i < l; i++) { - Ext.getBody().mask(`Deleting ${i+1}/${l} Assets`) - // Edge case to handle when the selected record was changed (e.g., stats updated) - // while still selected, then is deleted - const thisRecord = me.store.getById(assetRecords[i].id) - apiAsset = await Ext.Ajax.requestPromise({ - responseType: 'json', - url: `${STIGMAN.Env.apiBase}/assets/${thisRecord.data.assetId}`, - method: 'DELETE' - }) - me.store.remove(thisRecord) - } - SM.Dispatcher.fireEvent('assetdeleted', apiAsset) //only need the last deleted asset + const assetIds = assetRecords.map( r => r.data.assetId) + Ext.getBody().mask(`Deleting ${assetRecords.length} Assets`) + await Ext.Ajax.requestPromise({ + responseType: 'json', + url: `${STIGMAN.Env.apiBase}/assets?collectionId=${me.collectionId}`, + method: 'PATCH', + jsonData: { + operation: 'delete', + assetIds + } + }) + me.store.suspendEvents(false) + // Might need to handle edge case when the selected record was changed (e.g., stats updated) while still selected, then is deleted + me.store.remove(assetRecords) + me.store.resumeEvents() + + SM.Dispatcher.fireEvent('assetdeleted', {collection: {collectionId: me.collectionId}}) // mock an Asset for collectionManager.onAssetEvent } } catch (e) {