Fix #34: Allow non-admins to use blocklist-search under qualifying conditions
Build and Push Docker Image / build (push) Successful in 46s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 1m40s
CI / Security audit (push) Successful in 2m4s
CI / Swagger Validation & Coverage (push) Successful in 2m10s
CI / Tests & coverage (push) Successful in 2m23s

- Expose ARR ID fields (arrQueueId, arrType, arrInstanceUrl, arrContentId, arrContentType) to non-admins in DownloadMatcher.js for blocklist functionality
- Replace blanket admin check in dashboard.js with canBlocklist() validation and server-side API key lookup
- Update integration tests to reflect new permission model
- Non-admins can now blocklist downloads with import issues or stale low-availability torrents
This commit is contained in:
2026-05-22 22:13:24 +01:00
parent b77c0d6ec0
commit 614af9eb44
3 changed files with 144 additions and 32 deletions
+31 -7
View File
@@ -12,8 +12,9 @@ const TagMatcher = require('../services/TagMatcher');
const { buildUserDownloads } = require('../services/DownloadBuilder');
const { onHistoryUpdate, offHistoryUpdate } = require('../utils/historyFetcher');
const arrRetrieverRegistry = require('../utils/arrRetrievers');
const { getOmbiInstances } = require('../utils/config');
const { getOmbiInstances, getSonarrInstances, getRadarrInstances } = require('../utils/config');
const { extractRequestedUser, filterRequestsByUser } = require('../utils/ombiHelpers');
const { canBlocklist } = require('../services/DownloadAssembler');
// Track active SSE clients for disconnect cleanup
@@ -673,13 +674,9 @@ router.get('/stream', requireAuth, async (req, res) => {
router.post('/blocklist-search', requireAuth, async (req, res) => {
try {
const user = req.user;
if (!user.isAdmin) {
return res.status(403).json({ error: 'Admin access required' });
}
const { arrQueueId, arrType, arrInstanceUrl, arrInstanceKey, arrContentId, arrContentType } = req.body;
if (!arrQueueId || !arrType || !arrInstanceUrl || !arrInstanceKey || !arrContentId || !arrContentType) {
if (!arrQueueId || !arrType || !arrInstanceUrl || !arrContentId || !arrContentType) {
console.error('[Blocklist] Missing required fields:', { arrQueueId, arrType, arrInstanceUrl, hasKey: !!arrInstanceKey, arrContentId, arrContentType });
return res.status(400).json({ error: 'Missing required fields' });
}
@@ -687,7 +684,34 @@ router.post('/blocklist-search', requireAuth, async (req, res) => {
return res.status(400).json({ error: 'arrType must be sonarr or radarr' });
}
const headers = { 'X-Api-Key': arrInstanceKey };
// Look up the download to verify permission
const allDownloads = await downloadClientRegistry.getAllDownloads();
const download = allDownloads.find(d => d.arrQueueId === arrQueueId && d.arrType === arrType);
if (!download) {
console.error('[Blocklist] Download not found:', { arrQueueId, arrType });
return res.status(403).json({ error: 'Download not found or permission denied' });
}
// Check if user can blocklist this download
if (!canBlocklist(download, user.isAdmin)) {
console.log('[Blocklist] Permission denied:', { user: user.name, isAdmin: user.isAdmin, arrQueueId, arrType });
return res.status(403).json({ error: 'Permission denied: admin or qualifying conditions required' });
}
// Resolve API key: use provided key (admin) or look up from instance config (non-admin)
let apiKey = arrInstanceKey;
if (!apiKey) {
const instances = arrType === 'sonarr' ? getSonarrInstances() : getRadarrInstances();
const instance = instances.find(inst => inst.url === arrInstanceUrl);
if (!instance || !instance.apiKey) {
console.error('[Blocklist] Instance not found or missing API key:', { arrType, arrInstanceUrl });
return res.status(400).json({ error: 'Instance not found or missing API key' });
}
apiKey = instance.apiKey;
}
const headers = { 'X-Api-Key': apiKey };
// Step 1: Remove from queue with blocklist=true
await axios.delete(`${arrInstanceUrl}/api/v3/queue/${arrQueueId}`, {