feat: implement group-based access control (Admin, Standard_User, Leadership, Read_Only)
- Add user_group migration and created_by column migration - Replace requireRole middleware with requireGroup - Update all backend routes to use group-based authorization - Add Standard_User conditional delete with ownership, state, and compliance checks - Add cascade impact check for CVE deletes - Update AuthContext with group-based permission helpers - Update all frontend components for group-based rendering - Update UserManagement UI with group dropdown, confirmation dialogs, self-demotion prevention
This commit is contained in:
@@ -12,7 +12,7 @@ function requireAuth(db) {
|
||||
try {
|
||||
const session = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
`SELECT s.*, u.id as user_id, u.username, u.email, u.role, u.is_active
|
||||
`SELECT s.*, u.id as user_id, u.username, u.email, u.role, u.user_group, u.is_active
|
||||
FROM sessions s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.session_id = ? AND s.expires_at > datetime('now')`,
|
||||
@@ -37,7 +37,8 @@ function requireAuth(db) {
|
||||
id: session.user_id,
|
||||
username: session.username,
|
||||
email: session.email,
|
||||
role: session.role
|
||||
role: session.role,
|
||||
group: session.user_group
|
||||
};
|
||||
|
||||
next();
|
||||
@@ -48,18 +49,18 @@ function requireAuth(db) {
|
||||
};
|
||||
}
|
||||
|
||||
// Require specific role(s)
|
||||
function requireRole(...allowedRoles) {
|
||||
// Require specific group(s)
|
||||
function requireGroup(...allowedGroups) {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
if (!allowedRoles.includes(req.user.role)) {
|
||||
if (!allowedGroups.includes(req.user.group)) {
|
||||
return res.status(403).json({
|
||||
error: 'Insufficient permissions',
|
||||
required: allowedRoles,
|
||||
current: req.user.role
|
||||
required: allowedGroups,
|
||||
current: req.user.group
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,4 +68,4 @@ function requireRole(...allowedRoles) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { requireAuth, requireRole };
|
||||
module.exports = { requireAuth, requireGroup };
|
||||
|
||||
76
backend/migrations/add_created_by_columns.js
Normal file
76
backend/migrations/add_created_by_columns.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// Migration: Add created_by column to cves, archer_tickets, and jira_tickets tables
|
||||
// Stores the user ID of the creator for ownership-based delete checks.
|
||||
// Idempotent — safe to run multiple times.
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Run the migration against the given database instance.
|
||||
* Exported for testing with in-memory databases.
|
||||
* @param {sqlite3.Database} db
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function runMigration(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tables = ['cves', 'archer_tickets', 'jira_tickets'];
|
||||
let completed = 0;
|
||||
|
||||
db.serialize(() => {
|
||||
tables.forEach((table) => {
|
||||
db.all(`PRAGMA table_info(${table})`, (err, columns) => {
|
||||
if (err) {
|
||||
// Table may not exist yet — skip gracefully
|
||||
console.log(`⚠ Could not inspect ${table}: ${err.message} — skipping`);
|
||||
completed++;
|
||||
if (completed === tables.length) resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const hasCreatedBy = columns.some(col => col.name === 'created_by');
|
||||
|
||||
if (hasCreatedBy) {
|
||||
console.log(`✓ ${table}.created_by already exists — skipping`);
|
||||
completed++;
|
||||
if (completed === tables.length) resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
db.run(
|
||||
`ALTER TABLE ${table} ADD COLUMN created_by INTEGER REFERENCES users(id)`,
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
console.log(`✓ Added created_by column to ${table}`);
|
||||
completed++;
|
||||
if (completed === tables.length) resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Run directly if executed as a script
|
||||
if (require.main === module) {
|
||||
const dbPath = path.join(__dirname, '..', 'cve_database.db');
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
console.log('Starting add_created_by_columns migration...');
|
||||
|
||||
runMigration(db)
|
||||
.then(() => {
|
||||
console.log('Migration complete!');
|
||||
db.close(() => {
|
||||
console.log('Database connection closed.');
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Migration failed:', err);
|
||||
db.close();
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { runMigration };
|
||||
119
backend/migrations/add_user_groups.js
Normal file
119
backend/migrations/add_user_groups.js
Normal file
@@ -0,0 +1,119 @@
|
||||
// Migration: Add user_group column to users table and map legacy roles
|
||||
// Mapping: admin→Admin, editor→Standard_User, viewer→Read_Only
|
||||
// NULL/unrecognized roles default to Read_Only
|
||||
// Idempotent — safe to run multiple times
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Run the migration against the given database instance.
|
||||
* Exported for testing with in-memory databases.
|
||||
* @param {sqlite3.Database} db
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function runMigration(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
// Check if user_group column already exists
|
||||
db.all("PRAGMA table_info(users)", (err, columns) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasUserGroup = columns.some(col => col.name === 'user_group');
|
||||
|
||||
if (hasUserGroup) {
|
||||
console.log('✓ user_group column already exists — skipping migration');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Adding user_group column to users table...');
|
||||
|
||||
// SQLite doesn't support ADD COLUMN with CHECK inline in all versions,
|
||||
// so we add the column first, map values, then recreate with constraint.
|
||||
// However, SQLite also doesn't support ALTER TABLE ADD CONSTRAINT.
|
||||
// Strategy: add column, map values, create index.
|
||||
// The CHECK constraint is enforced via table rebuild.
|
||||
|
||||
db.run(
|
||||
`ALTER TABLE users ADD COLUMN user_group VARCHAR(20) NOT NULL DEFAULT 'Read_Only'`,
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
console.log('✓ Added user_group column');
|
||||
|
||||
// Map existing roles to groups
|
||||
db.run(
|
||||
`UPDATE users SET user_group = 'Admin' WHERE role = 'admin'`,
|
||||
function(err) {
|
||||
if (err) { reject(err); return; }
|
||||
console.log(` ✓ Mapped ${this.changes} admin(s) → Admin`);
|
||||
|
||||
db.run(
|
||||
`UPDATE users SET user_group = 'Standard_User' WHERE role = 'editor'`,
|
||||
function(err) {
|
||||
if (err) { reject(err); return; }
|
||||
console.log(` ✓ Mapped ${this.changes} editor(s) → Standard_User`);
|
||||
|
||||
db.run(
|
||||
`UPDATE users SET user_group = 'Read_Only' WHERE role = 'viewer'`,
|
||||
function(err) {
|
||||
if (err) { reject(err); return; }
|
||||
console.log(` ✓ Mapped ${this.changes} viewer(s) → Read_Only`);
|
||||
|
||||
// Map NULL or unrecognized roles to Read_Only
|
||||
db.run(
|
||||
`UPDATE users SET user_group = 'Read_Only' WHERE user_group = 'Read_Only' AND role NOT IN ('admin', 'editor', 'viewer')`,
|
||||
function(err) {
|
||||
if (err) { reject(err); return; }
|
||||
console.log(` ✓ Mapped ${this.changes} unrecognized role(s) → Read_Only`);
|
||||
|
||||
// Create index on user_group
|
||||
db.run(
|
||||
`CREATE INDEX IF NOT EXISTS idx_users_user_group ON users(user_group)`,
|
||||
(err) => {
|
||||
if (err) { reject(err); return; }
|
||||
console.log('✓ Created idx_users_user_group index');
|
||||
console.log('Migration complete!');
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Run directly if executed as a script
|
||||
if (require.main === module) {
|
||||
const dbPath = path.join(__dirname, '..', 'cve_database.db');
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
console.log('Starting add_user_groups migration...');
|
||||
|
||||
runMigration(db)
|
||||
.then(() => {
|
||||
db.close(() => {
|
||||
console.log('Database connection closed.');
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Migration failed:', err);
|
||||
db.close();
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { runMigration };
|
||||
@@ -1,6 +1,6 @@
|
||||
// routes/archerTickets.js
|
||||
const express = require('express');
|
||||
const { requireAuth, requireRole } = require('../middleware/auth');
|
||||
const { requireAuth, requireGroup } = require('../middleware/auth');
|
||||
const logAudit = require('../helpers/auditLog');
|
||||
|
||||
// Validation helpers
|
||||
@@ -48,7 +48,7 @@ function createArcherTicketsRouter(db) {
|
||||
});
|
||||
|
||||
// Create Archer ticket
|
||||
router.post('/', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
router.post('/', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { exc_number, archer_url, status, cve_id, vendor } = req.body;
|
||||
|
||||
// Validation
|
||||
@@ -74,9 +74,9 @@ function createArcherTicketsRouter(db) {
|
||||
const validatedStatus = status || 'Draft';
|
||||
|
||||
db.run(
|
||||
`INSERT INTO archer_tickets (exc_number, archer_url, status, cve_id, vendor)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[exc_number.trim(), archer_url || null, validatedStatus, cve_id, vendor],
|
||||
`INSERT INTO archer_tickets (exc_number, archer_url, status, cve_id, vendor, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[exc_number.trim(), archer_url || null, validatedStatus, cve_id, vendor, req.user.id],
|
||||
function(err) {
|
||||
if (err) {
|
||||
console.error('Error creating Archer ticket:', err);
|
||||
@@ -104,7 +104,7 @@ function createArcherTicketsRouter(db) {
|
||||
});
|
||||
|
||||
// Update Archer ticket
|
||||
router.put('/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
router.put('/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { exc_number, archer_url, status } = req.body;
|
||||
|
||||
@@ -184,8 +184,29 @@ function createArcherTicketsRouter(db) {
|
||||
});
|
||||
});
|
||||
|
||||
// Helper: perform the actual Archer ticket deletion
|
||||
function performArcherDelete(db, req, res, id, ticket) {
|
||||
db.run('DELETE FROM archer_tickets WHERE id = ?', [id], function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Internal server error.' });
|
||||
}
|
||||
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
action: 'DELETE_ARCHER_TICKET',
|
||||
targetType: 'archer_ticket',
|
||||
targetId: id,
|
||||
details: { deleted: ticket },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
res.json({ message: 'Archer ticket deleted successfully' });
|
||||
});
|
||||
}
|
||||
|
||||
// Delete Archer ticket
|
||||
router.delete('/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
router.delete('/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
db.get('SELECT * FROM archer_tickets WHERE id = ?', [id], (err, ticket) => {
|
||||
@@ -197,23 +218,45 @@ function createArcherTicketsRouter(db) {
|
||||
return res.status(404).json({ error: 'Archer ticket not found.' });
|
||||
}
|
||||
|
||||
db.run('DELETE FROM archer_tickets WHERE id = ?', [id], function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Internal server error.' });
|
||||
// Admin bypasses all delete restrictions
|
||||
if (req.user.group === 'Admin') {
|
||||
return performArcherDelete(db, req, res, id, ticket);
|
||||
}
|
||||
|
||||
// Standard_User: ownership check
|
||||
if (ticket.created_by && ticket.created_by !== req.user.id) {
|
||||
return res.status(403).json({ error: 'You can only delete resources you created' });
|
||||
}
|
||||
|
||||
// Standard_User: compliance linkage check
|
||||
const excNumber = ticket.exc_number;
|
||||
db.all(
|
||||
`SELECT ci.id, ci.extra_json
|
||||
FROM compliance_items ci
|
||||
JOIN compliance_uploads cu ON ci.upload_id = cu.id
|
||||
WHERE ci.status = 'active' AND ci.extra_json LIKE ?`,
|
||||
[`%${excNumber}%`],
|
||||
(compErr, compLinks) => {
|
||||
// If compliance_items table doesn't exist yet, treat as no linkage
|
||||
if (compErr && compErr.message && compErr.message.includes('no such table')) {
|
||||
compLinks = [];
|
||||
} else if (compErr) {
|
||||
console.error(compErr);
|
||||
return res.status(500).json({ error: 'Internal server error.' });
|
||||
}
|
||||
|
||||
const isLinked = (compLinks || []).some(cl => {
|
||||
const json = cl.extra_json || '';
|
||||
return json.includes(excNumber);
|
||||
});
|
||||
|
||||
if (isLinked) {
|
||||
return res.status(403).json({ error: 'Cannot delete ticket linked to compliance report. Contact an admin.' });
|
||||
}
|
||||
|
||||
return performArcherDelete(db, req, res, id, ticket);
|
||||
}
|
||||
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
action: 'DELETE_ARCHER_TICKET',
|
||||
targetType: 'archer_ticket',
|
||||
targetId: id,
|
||||
details: { deleted: ticket },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
res.json({ message: 'Archer ticket deleted successfully' });
|
||||
});
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Audit Log Routes (Admin only)
|
||||
const express = require('express');
|
||||
|
||||
function createAuditLogRouter(db, requireAuth, requireRole) {
|
||||
function createAuditLogRouter(db, requireAuth, requireGroup) {
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require admin role
|
||||
router.use(requireAuth(db), requireRole('admin'));
|
||||
// All routes require Admin group
|
||||
router.use(requireAuth(db), requireGroup('Admin'));
|
||||
|
||||
// Get paginated audit logs with filters
|
||||
router.get('/', async (req, res) => {
|
||||
|
||||
@@ -110,7 +110,7 @@ function createAuthRouter(db, logAudit) {
|
||||
action: 'login',
|
||||
entityType: 'auth',
|
||||
entityId: null,
|
||||
details: { role: user.role },
|
||||
details: { group: user.user_group },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ function createAuthRouter(db, logAudit) {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
group: user.user_group
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -183,7 +183,7 @@ function createAuthRouter(db, logAudit) {
|
||||
try {
|
||||
const session = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
`SELECT s.*, u.id as user_id, u.username, u.email, u.role, u.is_active
|
||||
`SELECT s.*, u.id as user_id, u.username, u.email, u.user_group, u.is_active
|
||||
FROM sessions s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.session_id = ? AND s.expires_at > datetime('now')`,
|
||||
@@ -210,7 +210,7 @@ function createAuthRouter(db, logAudit) {
|
||||
id: session.user_id,
|
||||
username: session.username,
|
||||
email: session.email,
|
||||
role: session.role
|
||||
group: session.user_group
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -213,7 +213,7 @@ function groupByHostname(rows, noteHostnames) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Router factory
|
||||
// ---------------------------------------------------------------------------
|
||||
function createComplianceRouter(db, upload, requireAuth, requireRole) {
|
||||
function createComplianceRouter(db, upload, requireAuth, requireGroup) {
|
||||
const router = express.Router();
|
||||
|
||||
// Idempotent column additions — errors mean column already exists, which is fine
|
||||
@@ -228,7 +228,7 @@ function createComplianceRouter(db, upload, requireAuth, requireRole) {
|
||||
// Parse the uploaded xlsx, compute diff, save parsed data to a temp JSON.
|
||||
// Returns diff counts + tempFile path for the commit step.
|
||||
// -----------------------------------------------------------------------
|
||||
router.post('/preview', requireRole('editor', 'admin'), (req, res) => {
|
||||
router.post('/preview', requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
upload.single('file')(req, res, async (uploadErr) => {
|
||||
if (uploadErr) {
|
||||
return res.status(400).json({ error: uploadErr.message });
|
||||
@@ -291,7 +291,7 @@ function createComplianceRouter(db, upload, requireAuth, requireRole) {
|
||||
// Commit a previewed upload to the DB.
|
||||
// Body: { tempFile, filename, report_date }
|
||||
// -----------------------------------------------------------------------
|
||||
router.post('/commit', requireRole('editor', 'admin'), async (req, res) => {
|
||||
router.post('/commit', requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
const { tempFile, filename, report_date } = req.body;
|
||||
|
||||
if (!tempFile || typeof tempFile !== 'string') {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
const express = require('express');
|
||||
const https = require('https');
|
||||
const { requireRole } = require('../middleware/auth');
|
||||
const { requireGroup } = require('../middleware/auth');
|
||||
|
||||
const IVANTI_URL_BASE = 'https://platform4.risksense.com/api/v1';
|
||||
const SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
@@ -899,7 +899,7 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
|
||||
// PUT /:findingId/override — save or clear a field override (editor/admin only)
|
||||
const OVERRIDE_ALLOWED = ['hostName', 'dns'];
|
||||
router.put('/:findingId/override', requireRole('editor', 'admin'), (req, res) => {
|
||||
router.put('/:findingId/override', requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { findingId } = req.params;
|
||||
const { field, value } = req.body;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { requireAuth, requireRole } = require('../middleware/auth');
|
||||
const { requireAuth, requireGroup } = require('../middleware/auth');
|
||||
const logAudit = require('../helpers/auditLog');
|
||||
|
||||
function createKnowledgeBaseRouter(db, upload) {
|
||||
@@ -40,7 +40,7 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
}
|
||||
|
||||
// POST /api/knowledge-base/upload - Upload new document
|
||||
router.post('/upload', requireAuth(db), requireRole(db, 'editor', 'admin'), (req, res, next) => {
|
||||
router.post('/upload', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res, next) => {
|
||||
upload.single('file')(req, res, (err) => {
|
||||
if (err) {
|
||||
console.error('[KB Upload] Multer error:', err);
|
||||
@@ -302,7 +302,7 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
});
|
||||
|
||||
// DELETE /api/knowledge-base/:id - Delete article
|
||||
router.delete('/:id', requireAuth(db), requireRole(db, 'editor', 'admin'), (req, res) => {
|
||||
router.delete('/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const sql = 'SELECT file_path, title FROM knowledge_base WHERE id = ?';
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
function createUsersRouter(db, requireAuth, requireGroup, logAudit) {
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require admin role
|
||||
router.use(requireAuth(db), requireRole('admin'));
|
||||
// All routes require Admin group
|
||||
router.use(requireAuth(db), requireGroup('Admin'));
|
||||
|
||||
// Get all users
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const users = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
`SELECT id, username, email, role, is_active, created_at, last_login
|
||||
`SELECT id, username, email, user_group AS 'group', is_active, created_at, last_login
|
||||
FROM users ORDER BY created_at DESC`,
|
||||
(err, rows) => {
|
||||
if (err) reject(err);
|
||||
@@ -33,7 +33,7 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
try {
|
||||
const user = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
`SELECT id, username, email, role, is_active, created_at, last_login
|
||||
`SELECT id, username, email, user_group AS 'group', is_active, created_at, last_login
|
||||
FROM users WHERE id = ?`,
|
||||
[req.params.id],
|
||||
(err, row) => {
|
||||
@@ -56,14 +56,17 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
|
||||
// Create new user
|
||||
router.post('/', async (req, res) => {
|
||||
const { username, email, password, role } = req.body;
|
||||
const { username, email, password, group } = req.body;
|
||||
const VALID_GROUPS = ['Admin', 'Standard_User', 'Leadership', 'Read_Only'];
|
||||
|
||||
if (!username || !email || !password) {
|
||||
return res.status(400).json({ error: 'Username, email, and password are required' });
|
||||
}
|
||||
|
||||
if (role && !['admin', 'editor', 'viewer'].includes(role)) {
|
||||
return res.status(400).json({ error: 'Invalid role. Must be admin, editor, or viewer' });
|
||||
const userGroup = group || 'Read_Only';
|
||||
|
||||
if (!VALID_GROUPS.includes(userGroup)) {
|
||||
return res.status(400).json({ error: 'Invalid group. Must be one of: Admin, Standard_User, Leadership, Read_Only' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -71,9 +74,9 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
`INSERT INTO users (username, email, password_hash, role)
|
||||
`INSERT INTO users (username, email, password_hash, user_group)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[username, email, passwordHash, role || 'viewer'],
|
||||
[username, email, passwordHash, userGroup],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
@@ -87,7 +90,7 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
action: 'user_create',
|
||||
entityType: 'user',
|
||||
entityId: String(result.id),
|
||||
details: { created_username: username, role: role || 'viewer' },
|
||||
details: { created_username: username, group: userGroup },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
@@ -97,7 +100,7 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
id: result.id,
|
||||
username,
|
||||
email,
|
||||
role: role || 'viewer'
|
||||
group: userGroup
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -111,12 +114,18 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
|
||||
// Update user
|
||||
router.patch('/:id', async (req, res) => {
|
||||
const { username, email, password, role, is_active } = req.body;
|
||||
const { username, email, password, group, is_active } = req.body;
|
||||
const VALID_GROUPS = ['Admin', 'Standard_User', 'Leadership', 'Read_Only'];
|
||||
const userId = req.params.id;
|
||||
|
||||
// Prevent self-demotion from admin
|
||||
if (userId == req.user.id && role && role !== 'admin') {
|
||||
return res.status(400).json({ error: 'Cannot remove your own admin role' });
|
||||
// Validate group if provided
|
||||
if (group && !VALID_GROUPS.includes(group)) {
|
||||
return res.status(400).json({ error: 'Invalid group. Must be one of: Admin, Standard_User, Leadership, Read_Only' });
|
||||
}
|
||||
|
||||
// Prevent admin self-demotion
|
||||
if (userId == req.user.id && group && group !== 'Admin') {
|
||||
return res.status(400).json({ error: 'Cannot remove your own admin group' });
|
||||
}
|
||||
|
||||
// Prevent self-deactivation
|
||||
@@ -125,6 +134,22 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch current user record before update (needed for group change audit)
|
||||
const currentUser = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT user_group FROM users WHERE id = ?',
|
||||
[userId],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!currentUser) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const updates = [];
|
||||
const values = [];
|
||||
|
||||
@@ -141,12 +166,9 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
updates.push('password_hash = ?');
|
||||
values.push(passwordHash);
|
||||
}
|
||||
if (role) {
|
||||
if (!['admin', 'editor', 'viewer'].includes(role)) {
|
||||
return res.status(400).json({ error: 'Invalid role' });
|
||||
}
|
||||
updates.push('role = ?');
|
||||
values.push(role);
|
||||
if (group) {
|
||||
updates.push('user_group = ?');
|
||||
values.push(group);
|
||||
}
|
||||
if (typeof is_active === 'boolean') {
|
||||
updates.push('is_active = ?');
|
||||
@@ -173,7 +195,7 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
const updatedFields = {};
|
||||
if (username) updatedFields.username = username;
|
||||
if (email) updatedFields.email = email;
|
||||
if (role) updatedFields.role = role;
|
||||
if (group) updatedFields.group = group;
|
||||
if (typeof is_active === 'boolean') updatedFields.is_active = is_active;
|
||||
if (password) updatedFields.password_changed = true;
|
||||
|
||||
@@ -187,6 +209,22 @@ function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
// Log specific audit entry for group changes
|
||||
if (group && group !== currentUser.user_group) {
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'user_group_change',
|
||||
entityType: 'user',
|
||||
entityId: String(userId),
|
||||
details: {
|
||||
previous_group: currentUser.user_group,
|
||||
new_group: group
|
||||
},
|
||||
ipAddress: req.ip
|
||||
});
|
||||
}
|
||||
|
||||
// If user was deactivated, delete their sessions
|
||||
if (is_active === false) {
|
||||
await new Promise((resolve) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Auth imports
|
||||
const { requireAuth, requireRole } = require('./middleware/auth');
|
||||
const { requireAuth, requireGroup } = require('./middleware/auth');
|
||||
const createAuthRouter = require('./routes/auth');
|
||||
const createUsersRouter = require('./routes/users');
|
||||
const createAuditLogRouter = require('./routes/auditLog');
|
||||
@@ -161,10 +161,10 @@ const db = new sqlite3.Database('./cve_database.db', (err) => {
|
||||
app.use('/api/auth', createAuthRouter(db, logAudit));
|
||||
|
||||
// User management routes (admin only)
|
||||
app.use('/api/users', createUsersRouter(db, requireAuth, requireRole, logAudit));
|
||||
app.use('/api/users', createUsersRouter(db, requireAuth, requireGroup, logAudit));
|
||||
|
||||
// Audit log routes (admin only)
|
||||
app.use('/api/audit-logs', createAuditLogRouter(db, requireAuth, requireRole));
|
||||
app.use('/api/audit-logs', createAuditLogRouter(db, requireAuth, requireGroup));
|
||||
|
||||
// NVD lookup routes (authenticated users)
|
||||
app.use('/api/nvd', createNvdLookupRouter(db, requireAuth));
|
||||
@@ -224,7 +224,7 @@ app.use('/api/ivanti/todo-queue', createIvantiTodoQueueRouter(db, requireAuth));
|
||||
app.use('/api/ivanti/archive', createIvantiArchiveRouter(db, requireAuth));
|
||||
|
||||
// AEO compliance routes — xlsx upload, non-compliant item tracking, notes
|
||||
app.use('/api/compliance', createComplianceRouter(db, upload, requireAuth, requireRole));
|
||||
app.use('/api/compliance', createComplianceRouter(db, upload, requireAuth, requireGroup));
|
||||
|
||||
// ========== CVE ENDPOINTS ==========
|
||||
|
||||
@@ -353,7 +353,7 @@ app.get('/api/cves/compliance', requireAuth(db), (req, res) => {
|
||||
});
|
||||
|
||||
// Create new CVE entry - ALLOW MULTIPLE VENDORS (editor or admin)
|
||||
app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
app.post('/api/cves', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { cve_id, vendor, severity, description, published_date } = req.body;
|
||||
|
||||
// Input validation
|
||||
@@ -374,11 +374,11 @@ app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO cves (cve_id, vendor, severity, description, published_date)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO cves (cve_id, vendor, severity, description, published_date, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
db.run(query, [cve_id, vendor, severity, description, published_date], function(err) {
|
||||
db.run(query, [cve_id, vendor, severity, description, published_date, req.user.id], function(err) {
|
||||
if (err) {
|
||||
console.error('DATABASE ERROR:', err);
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
@@ -407,7 +407,7 @@ app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res
|
||||
|
||||
|
||||
// Update CVE status (editor or admin)
|
||||
app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
app.patch('/api/cves/:cveId/status', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { cveId } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
@@ -435,7 +435,7 @@ app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'adm
|
||||
});
|
||||
|
||||
// Bulk sync CVE data from NVD (editor or admin)
|
||||
app.post('/api/cves/nvd-sync', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
app.post('/api/cves/nvd-sync', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { updates } = req.body;
|
||||
if (!Array.isArray(updates) || updates.length === 0) {
|
||||
return res.status(400).json({ error: 'No updates provided' });
|
||||
@@ -505,7 +505,7 @@ app.post('/api/cves/nvd-sync', requireAuth(db), requireRole('editor', 'admin'),
|
||||
// ========== CVE EDIT & DELETE ENDPOINTS ==========
|
||||
|
||||
// Edit single CVE entry (editor or admin)
|
||||
app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
app.put('/api/cves/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { cve_id, vendor, severity, description, published_date, status } = req.body;
|
||||
|
||||
@@ -649,7 +649,7 @@ app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req,
|
||||
});
|
||||
|
||||
// Delete entire CVE - all vendors (editor or admin) - MUST be before /:id route
|
||||
app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { cveId } = req.params;
|
||||
|
||||
// Get all rows for this CVE ID to know what we're deleting
|
||||
@@ -657,6 +657,151 @@ app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireRole('editor',
|
||||
if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||
if (!rows || rows.length === 0) return res.status(404).json({ error: 'No CVE entries found for this CVE ID' });
|
||||
|
||||
// Ownership check: Standard_User can only delete CVEs they created
|
||||
if (req.user.group === 'Standard_User') {
|
||||
const notOwned = rows.some(row => row.created_by !== req.user.id);
|
||||
if (notOwned) {
|
||||
return res.status(403).json({ error: 'You can only delete resources you created' });
|
||||
}
|
||||
|
||||
// Cascade impact check for Standard_User
|
||||
// Query all three cascade-deleted resource types in parallel
|
||||
db.all('SELECT id, exc_number, cve_id, vendor FROM archer_tickets WHERE cve_id = ?', [cveId], (archerErr, archerTickets) => {
|
||||
if (archerErr) { console.error(archerErr); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||
|
||||
db.all('SELECT id, cve_id, vendor, ticket_key, status FROM jira_tickets WHERE cve_id = ?', [cveId], (jiraErr, jiraTickets) => {
|
||||
// If jira_tickets table doesn't exist yet, treat as empty
|
||||
if (jiraErr && jiraErr.message && jiraErr.message.includes('no such table')) {
|
||||
jiraTickets = [];
|
||||
} else if (jiraErr) {
|
||||
console.error(jiraErr);
|
||||
return res.status(500).json({ error: 'Internal server error.' });
|
||||
}
|
||||
|
||||
db.all('SELECT id, name, type FROM documents WHERE cve_id = ?', [cveId], (docErr, docs) => {
|
||||
if (docErr) { console.error(docErr); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||
|
||||
const allTickets = [
|
||||
...(archerTickets || []).map(t => ({ ...t, source: 'archer', key: t.exc_number })),
|
||||
...(jiraTickets || []).map(t => ({ ...t, source: 'jira', key: t.ticket_key }))
|
||||
];
|
||||
|
||||
// If no tickets at all, no compliance linkage possible — return cascade info
|
||||
if (allTickets.length === 0) {
|
||||
return res.json({
|
||||
cascade_impact: {
|
||||
archer_tickets: [],
|
||||
jira_tickets: [],
|
||||
documents: (docs || []).map(d => ({ id: d.id, name: d.name, type: d.type })),
|
||||
blocked: false,
|
||||
blocked_reason: null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check compliance linkage for each ticket
|
||||
// A ticket is compliance-linked if its key (exc_number or ticket_key) or cve_id
|
||||
// appears in active compliance_items extra_json
|
||||
const likeConditions = [];
|
||||
const likeParams = [];
|
||||
for (const t of allTickets) {
|
||||
likeConditions.push('ci.extra_json LIKE ?');
|
||||
likeParams.push(`%${t.key}%`);
|
||||
}
|
||||
// Also check if the CVE ID itself appears in compliance extra_json
|
||||
likeConditions.push('ci.extra_json LIKE ?');
|
||||
likeParams.push(`%${cveId}%`);
|
||||
|
||||
db.all(
|
||||
`SELECT ci.id, ci.extra_json, cu.report_date
|
||||
FROM compliance_items ci
|
||||
JOIN compliance_uploads cu ON ci.upload_id = cu.id
|
||||
WHERE ci.status = 'active' AND (${likeConditions.join(' OR ')})`,
|
||||
likeParams,
|
||||
(compErr, compLinks) => {
|
||||
// If compliance_items table doesn't exist yet, treat as no linkage
|
||||
if (compErr && compErr.message && compErr.message.includes('no such table')) {
|
||||
compLinks = [];
|
||||
} else if (compErr) {
|
||||
console.error(compErr);
|
||||
return res.status(500).json({ error: 'Internal server error.' });
|
||||
}
|
||||
|
||||
// Determine which tickets are compliance-linked by checking extra_json matches
|
||||
const linkedTicketKeys = new Set();
|
||||
for (const cl of (compLinks || [])) {
|
||||
const json = cl.extra_json || '';
|
||||
for (const t of allTickets) {
|
||||
if (json.includes(t.key)) {
|
||||
linkedTicketKeys.add(`${t.source}:${t.id}`);
|
||||
}
|
||||
}
|
||||
// If CVE ID itself is in compliance data, all tickets are considered linked
|
||||
if (json.includes(cveId)) {
|
||||
for (const t of allTickets) {
|
||||
linkedTicketKeys.add(`${t.source}:${t.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const archerTicketsResult = (archerTickets || []).map(t => ({
|
||||
id: t.id,
|
||||
exc_number: t.exc_number,
|
||||
compliance_linked: linkedTicketKeys.has(`archer:${t.id}`)
|
||||
}));
|
||||
|
||||
const jiraTicketsResult = (jiraTickets || []).map(t => ({
|
||||
id: t.id,
|
||||
ticket_key: t.ticket_key,
|
||||
compliance_linked: linkedTicketKeys.has(`jira:${t.id}`)
|
||||
}));
|
||||
|
||||
const documentsResult = (docs || []).map(d => ({
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
type: d.type
|
||||
}));
|
||||
|
||||
const hasComplianceLink = archerTicketsResult.some(t => t.compliance_linked)
|
||||
|| jiraTicketsResult.some(t => t.compliance_linked);
|
||||
|
||||
if (hasComplianceLink) {
|
||||
const blockedArcher = archerTicketsResult.find(t => t.compliance_linked);
|
||||
const blockedJira = jiraTicketsResult.find(t => t.compliance_linked);
|
||||
const blockedLabel = blockedArcher
|
||||
? `Archer ticket ${blockedArcher.exc_number}`
|
||||
: `JIRA ticket ${blockedJira.ticket_key}`;
|
||||
return res.status(403).json({
|
||||
error: 'CVE deletion blocked: associated ticket linked to compliance report',
|
||||
cascade_impact: {
|
||||
archer_tickets: archerTicketsResult,
|
||||
jira_tickets: jiraTicketsResult,
|
||||
documents: documentsResult,
|
||||
blocked: true,
|
||||
blocked_reason: `${blockedLabel} is linked to a compliance report`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Not blocked — return cascade impact for frontend warning
|
||||
return res.json({
|
||||
cascade_impact: {
|
||||
archer_tickets: archerTicketsResult,
|
||||
jira_tickets: jiraTicketsResult,
|
||||
documents: documentsResult,
|
||||
blocked: false,
|
||||
blocked_reason: null
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
return; // Exit early — Standard_User flow handled above
|
||||
}
|
||||
|
||||
// Admin flow: proceed directly with deletion (no cascade check)
|
||||
// Delete all documents from DB
|
||||
db.run('DELETE FROM documents WHERE cve_id = ?', [cveId], (docErr) => {
|
||||
if (docErr) console.error('Error deleting documents:', docErr);
|
||||
@@ -689,13 +834,18 @@ app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireRole('editor',
|
||||
});
|
||||
|
||||
// Delete single CVE vendor entry (editor or admin)
|
||||
app.delete('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
app.delete('/api/cves/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
db.get('SELECT * FROM cves WHERE id = ?', [id], (err, cve) => {
|
||||
if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||
if (!cve) return res.status(404).json({ error: 'CVE entry not found' });
|
||||
|
||||
// Ownership check: Standard_User can only delete CVEs they created
|
||||
if (req.user.group === 'Standard_User' && cve.created_by !== req.user.id) {
|
||||
return res.status(403).json({ error: 'You can only delete resources you created' });
|
||||
}
|
||||
|
||||
// Delete associated documents from DB
|
||||
db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [cve.cve_id, cve.vendor], (docErr, docs) => {
|
||||
if (docErr) console.error('Error fetching documents:', docErr);
|
||||
@@ -771,7 +921,7 @@ app.get('/api/cves/:cveId/documents', requireAuth(db), (req, res) => {
|
||||
});
|
||||
|
||||
// Upload document - ADD ERROR HANDLING FOR MULTER (editor or admin)
|
||||
app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'admin'), (req, res, next) => {
|
||||
app.post('/api/cves/:cveId/documents', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res, next) => {
|
||||
upload.single('file')(req, res, (err) => {
|
||||
if (err) {
|
||||
console.error('Upload error:', err.message);
|
||||
@@ -879,7 +1029,7 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a
|
||||
});
|
||||
});
|
||||
// Delete document (admin only)
|
||||
app.delete('/api/documents/:id', requireAuth(db), requireRole('admin'), (req, res) => {
|
||||
app.delete('/api/documents/:id', requireAuth(db), requireGroup('Admin'), (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
// First get the file path to delete the actual file
|
||||
@@ -981,7 +1131,7 @@ app.get('/api/jira-tickets', requireAuth(db), (req, res) => {
|
||||
});
|
||||
|
||||
// Create JIRA ticket
|
||||
app.post('/api/jira-tickets', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
app.post('/api/jira-tickets', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { cve_id, vendor, ticket_key, url, summary, status } = req.body;
|
||||
|
||||
// Validation
|
||||
@@ -1007,11 +1157,11 @@ app.post('/api/jira-tickets', requireAuth(db), requireRole('editor', 'admin'), (
|
||||
const ticketStatus = status || 'Open';
|
||||
|
||||
const query = `
|
||||
INSERT INTO jira_tickets (cve_id, vendor, ticket_key, url, summary, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO jira_tickets (cve_id, vendor, ticket_key, url, summary, status, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
db.run(query, [cve_id, vendor, ticket_key.trim(), url || null, summary || null, ticketStatus], function(err) {
|
||||
db.run(query, [cve_id, vendor, ticket_key.trim(), url || null, summary || null, ticketStatus, req.user.id], function(err) {
|
||||
if (err) {
|
||||
console.error('Error creating JIRA ticket:', err);
|
||||
return res.status(500).json({ error: 'Internal server error.' });
|
||||
@@ -1035,7 +1185,7 @@ app.post('/api/jira-tickets', requireAuth(db), requireRole('editor', 'admin'), (
|
||||
});
|
||||
|
||||
// Update JIRA ticket
|
||||
app.put('/api/jira-tickets/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
app.put('/api/jira-tickets/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ticket_key, url, summary, status } = req.body;
|
||||
|
||||
@@ -1100,7 +1250,7 @@ app.put('/api/jira-tickets/:id', requireAuth(db), requireRole('editor', 'admin')
|
||||
});
|
||||
|
||||
// Delete JIRA ticket
|
||||
app.delete('/api/jira-tickets/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||
app.delete('/api/jira-tickets/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
db.get('SELECT * FROM jira_tickets WHERE id = ?', [id], (err, ticket) => {
|
||||
@@ -1112,24 +1262,66 @@ app.delete('/api/jira-tickets/:id', requireAuth(db), requireRole('editor', 'admi
|
||||
return res.status(404).json({ error: 'JIRA ticket not found.' });
|
||||
}
|
||||
|
||||
db.run('DELETE FROM jira_tickets WHERE id = ?', [id], function(deleteErr) {
|
||||
if (deleteErr) {
|
||||
console.error('Error deleting JIRA ticket:', deleteErr);
|
||||
return res.status(500).json({ error: 'Internal server error.' });
|
||||
// Admin bypasses all delete restrictions
|
||||
if (req.user.group === 'Admin') {
|
||||
return performJiraDelete();
|
||||
}
|
||||
|
||||
// Standard_User: ownership check
|
||||
if (ticket.created_by && ticket.created_by !== req.user.id) {
|
||||
return res.status(403).json({ error: 'You can only delete resources you created' });
|
||||
}
|
||||
|
||||
// Standard_User: compliance linkage check
|
||||
const ticketKey = ticket.ticket_key;
|
||||
db.all(
|
||||
`SELECT ci.id, ci.extra_json
|
||||
FROM compliance_items ci
|
||||
JOIN compliance_uploads cu ON ci.upload_id = cu.id
|
||||
WHERE ci.status = 'active' AND ci.extra_json LIKE ?`,
|
||||
[`%${ticketKey}%`],
|
||||
(compErr, compLinks) => {
|
||||
// If compliance_items table doesn't exist yet, treat as no linkage
|
||||
if (compErr && compErr.message && compErr.message.includes('no such table')) {
|
||||
compLinks = [];
|
||||
} else if (compErr) {
|
||||
console.error(compErr);
|
||||
return res.status(500).json({ error: 'Internal server error.' });
|
||||
}
|
||||
|
||||
const isLinked = (compLinks || []).some(cl => {
|
||||
const json = cl.extra_json || '';
|
||||
return json.includes(ticketKey);
|
||||
});
|
||||
|
||||
if (isLinked) {
|
||||
return res.status(403).json({ error: 'Cannot delete ticket linked to compliance report. Contact an admin.' });
|
||||
}
|
||||
|
||||
return performJiraDelete();
|
||||
}
|
||||
);
|
||||
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'jira_ticket_delete',
|
||||
entityType: 'jira_ticket',
|
||||
entityId: id,
|
||||
details: { ticket_key: ticket.ticket_key, cve_id: ticket.cve_id, vendor: ticket.vendor },
|
||||
ipAddress: req.ip
|
||||
function performJiraDelete() {
|
||||
db.run('DELETE FROM jira_tickets WHERE id = ?', [id], function(deleteErr) {
|
||||
if (deleteErr) {
|
||||
console.error('Error deleting JIRA ticket:', deleteErr);
|
||||
return res.status(500).json({ error: 'Internal server error.' });
|
||||
}
|
||||
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'jira_ticket_delete',
|
||||
entityType: 'jira_ticket',
|
||||
entityId: id,
|
||||
details: { ticket_key: ticket.ticket_key, cve_id: ticket.cve_id, vendor: ticket.vendor },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
res.json({ message: 'JIRA ticket deleted successfully' });
|
||||
});
|
||||
|
||||
res.json({ message: 'JIRA ticket deleted successfully' });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user