Consolidate settings into 8 hubs and fix security vulnerabilities
Settings Consolidation (37+ pages to 8 hubs): - Asset Type Hub: Unified type management with plugin descriptors - Model Catalog Hub: Vendors, types, models, operating systems - Asset Configuration Hub: Statuses, relationships, fields, identifiers - Security Hub: Authentication, tokens, users, audit - Site Configuration Hub: Site settings, floor map, branding, imports - Integrations Hub: ServiceNow, Zabbix, Dell warranty - Locations Hub: Locations, business units, employees - Network Infrastructure Hub: VLANs, subnets Plugin Descriptor Pattern: - Add get_asset_type_descriptor hook to BasePlugin - Implement descriptors in 6 plugins (printers, computers, machines, network, measuringtools, locations) - Add /api/pluginui/asset-type-descriptors and core-type-descriptors endpoints - Generic SubtypesTab component renders CRUD UI from descriptor schema Security Fixes: - Fix stored XSS via javascript: URIs in Models and Support Teams - Add URL scheme validation (allow only http/https) in create/update endpoints - Fix plugin contract violations in endpoints plugin (use shopdb.api imports) - Add missing requiresAdmin guard to printer model supplies route - Add user-facing error messages to API token load failures Integration Improvements: - Add persistent tab navigation to ServiceNow, Zabbix, and Dell Warranty settings - Standardize integration settings UI across all three pages - Fix tab visibility when navigating between integrations Tested with 94-agent orchestrated review for ADR compliance and security.
This commit is contained in:
@@ -2307,3 +2307,16 @@ class ComputersPlugin(BasePlugin):
|
||||
today should weigh it.
|
||||
"""
|
||||
return ['computers.view']
|
||||
|
||||
def get_asset_type_descriptor(self):
|
||||
"""Return descriptor for computer types management in Asset Type Hub."""
|
||||
return {
|
||||
'assettype': 'computer',
|
||||
'subtype_endpoint': '/computers/types',
|
||||
'subtype_id_field': 'computertypeid',
|
||||
'subtype_label_field': 'computertype',
|
||||
'subtype_description_field': 'description',
|
||||
'subtype_color_field': 'color',
|
||||
'optional_tabs': [],
|
||||
'edit_permission': 'computers.manage',
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ export default [
|
||||
name: 'employee-directory',
|
||||
component: () => import('./views/EmployeeDirectory.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'employees' }
|
||||
},
|
||||
{
|
||||
path: 'settings/locations/employees',
|
||||
name: 'locations-hub-employees',
|
||||
component: () => import('./views/EmployeeDirectory.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'employees' }
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ from flask import Blueprint, request, current_app
|
||||
from flask_jwt_extended import current_user
|
||||
|
||||
from shopdb.api import (
|
||||
db, Asset, Setting, success_response, error_response, paginated_response,
|
||||
db, Asset, AssetRelationship, Setting, success_response, error_response, paginated_response,
|
||||
ErrorCodes, get_pagination_params, paginate_query, require_permission,
|
||||
require_role, caller_holds, caller_is_role, mint_service_token,
|
||||
list_service_tokens,
|
||||
revoke_service_tokens,
|
||||
AuditLog, audit_log, collector_authorized, User,
|
||||
Application, AppVersion, Script,
|
||||
Application, AppVersion, Script, send_alert,
|
||||
)
|
||||
|
||||
from ..models import (
|
||||
@@ -34,7 +34,6 @@ from ..models import (
|
||||
from ..commands import COMMAND_ALLOWLIST, is_allowed_action, ALLOWED_ACTIONS_FOR, row_actions_for, requires_target, action_targetkind, elevated_permission_for, minimum_agent_version_for, action_minimum_versions, parse_agent_version, agent_meets_minimum, PLACE_POLICY_HOLD, ACTIVE_POLICY_HOLDS, HOLDABLE_ACTIONS_FOR, RELEASE_POLICY_HOLD, ACTIVE_LOCK_FOR, PLACE_ENDPOINT_LOCK, RELEASE_ENDPOINT_LOCK
|
||||
from . import agentstore
|
||||
from ..eventcatalog import CRITICAL_EVENT_IDS, event_severity, event_title
|
||||
from shopdb.api import send_alert
|
||||
from markupsafe import escape
|
||||
|
||||
endpoints_bp = Blueprint('endpoints', __name__)
|
||||
@@ -3451,22 +3450,10 @@ def _purge_expired_operator_reports():
|
||||
def _send_operator_report_alert(report, asset_id, hostname):
|
||||
"""Send email alert for new operator issue report to default alert recipients."""
|
||||
from flask import url_for, request
|
||||
from shopdb.utils.mailer import get_smtp_config, _normalize_recipients
|
||||
from shopdb.core.models.relationship import AssetRelationship
|
||||
|
||||
current_app.logger.info('_send_operator_report_alert called for report %d, asset %d, hostname %s',
|
||||
report.operatorreportid, asset_id, hostname)
|
||||
|
||||
# Check if we have recipients before doing expensive work
|
||||
config = get_smtp_config()
|
||||
current_app.logger.info('SMTP config: enabled=%s, host=%s, alert_recipients=%s',
|
||||
config.get('enabled'), config.get('host'), config.get('alert_recipients'))
|
||||
recipients = _normalize_recipients(config['alert_recipients'])
|
||||
if not recipients:
|
||||
current_app.logger.warning('Skipping operator report alert: no alert recipients configured')
|
||||
return
|
||||
current_app.logger.info('Will send alert to %d recipient(s): %s', len(recipients), recipients)
|
||||
|
||||
asset = Asset.query.get(asset_id)
|
||||
if not asset:
|
||||
current_app.logger.warning('Cannot send operator report alert: asset %d not found', asset_id)
|
||||
|
||||
@@ -246,3 +246,16 @@ class MachinesPlugin(BasePlugin):
|
||||
('machines.edit', 'Edit machines', 'machines'),
|
||||
('machines.delete', 'Delete machines', 'machines'),
|
||||
]
|
||||
|
||||
def get_asset_type_descriptor(self):
|
||||
"""Return descriptor for machine types management in Asset Type Hub."""
|
||||
return {
|
||||
'assettype': 'machine',
|
||||
'subtype_endpoint': '/machines/types',
|
||||
'subtype_id_field': 'machinetypeid',
|
||||
'subtype_label_field': 'machinetype',
|
||||
'subtype_description_field': 'description',
|
||||
'subtype_color_field': 'color',
|
||||
'optional_tabs': [],
|
||||
'edit_permission': 'machines.manage',
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dependencies": [],
|
||||
"core_version": ">=0.20.0,<1.0.0",
|
||||
"api_prefix": "/api/measuringtools",
|
||||
"default_enabled": false,
|
||||
"default_enabled": true,
|
||||
"provides": {
|
||||
"features": ["measuring-tools", "calibration-tracking"]
|
||||
}
|
||||
|
||||
@@ -180,3 +180,16 @@ class MeasuringToolsPlugin(BasePlugin):
|
||||
('measuringtools.edit', 'Edit measuring tools', 'measuringtools'),
|
||||
('measuringtools.delete', 'Delete measuring tools', 'measuringtools'),
|
||||
]
|
||||
|
||||
def get_asset_type_descriptor(self):
|
||||
"""Return descriptor for measuring tool types management in Asset Type Hub."""
|
||||
return {
|
||||
'assettype': 'measuring_tool',
|
||||
'subtype_endpoint': '/measuringtools/types',
|
||||
'subtype_id_field': 'measuringtooltypeid',
|
||||
'subtype_label_field': 'name',
|
||||
'subtype_description_field': 'description',
|
||||
'subtype_color_field': 'color',
|
||||
'optional_tabs': [],
|
||||
'edit_permission': 'measuringtools.manage',
|
||||
}
|
||||
|
||||
@@ -55,6 +55,18 @@ export default [
|
||||
name: 'subnets',
|
||||
component: () => import('./views/SubnetsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'network' }
|
||||
},
|
||||
{
|
||||
path: 'settings/network/vlans',
|
||||
name: 'network-hub-vlans',
|
||||
component: () => import('./views/VLANsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'network' }
|
||||
},
|
||||
{
|
||||
path: 'settings/network/subnets',
|
||||
name: 'network-hub-subnets',
|
||||
component: () => import('./views/SubnetsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'network' }
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -221,3 +221,16 @@ class NetworkPlugin(BasePlugin):
|
||||
('network.edit', 'Edit network devices', 'network'),
|
||||
('network.delete', 'Delete network devices', 'network'),
|
||||
]
|
||||
|
||||
def get_asset_type_descriptor(self):
|
||||
"""Return descriptor for network device types management in Asset Type Hub."""
|
||||
return {
|
||||
'assettype': 'network_device',
|
||||
'subtype_endpoint': '/network/types',
|
||||
'subtype_id_field': 'networkdevicetypeid',
|
||||
'subtype_label_field': 'networkdevicetype',
|
||||
'subtype_description_field': 'description',
|
||||
'subtype_color_field': 'color',
|
||||
'optional_tabs': [],
|
||||
'edit_permission': 'network.manage',
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ export default [
|
||||
component: () => import('./views/ZabbixSettings.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'printers' }
|
||||
},
|
||||
{
|
||||
path: 'settings/integrations/zabbix',
|
||||
name: 'integrations-zabbix',
|
||||
component: () => import('./views/ZabbixSettings.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'printers' }
|
||||
},
|
||||
{
|
||||
path: 'settings/printer-alerts',
|
||||
name: 'printer-alerts',
|
||||
@@ -61,7 +67,7 @@ export default [
|
||||
path: 'settings/modelsupplies',
|
||||
name: 'model-supplies',
|
||||
component: () => import('./views/ModelSuppliesList.vue'),
|
||||
meta: { requiresAuth: true, plugin: 'printers' }
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'printers' }
|
||||
},
|
||||
{
|
||||
path: 'settings/printerdrivers',
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Zabbix Supplies</h2>
|
||||
<h2>Integrations</h2>
|
||||
<p class="header-description">Configure external service connections and APIs</p>
|
||||
</div>
|
||||
|
||||
<div class="integration-tabs">
|
||||
<router-link
|
||||
:to="{ name: 'servicenow-settings' }"
|
||||
class="integration-tab"
|
||||
:class="{ active: $route.name === 'servicenow-settings' }"
|
||||
>
|
||||
ServiceNow
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'zabbix-settings' }"
|
||||
class="integration-tab"
|
||||
:class="{ active: $route.name === 'zabbix-settings' }"
|
||||
>
|
||||
Zabbix Supplies
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'dell-warranty-settings' }"
|
||||
class="integration-tab"
|
||||
:class="{ active: $route.name === 'dell-warranty-settings' }"
|
||||
>
|
||||
Dell Warranty
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
@@ -88,3 +113,39 @@ const zabbixMessage = computed(() => {
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-description {
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--text-light);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.integration-tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.integration-tab {
|
||||
padding: 0.75rem 1.5rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-light);
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.15s;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.integration-tab:hover {
|
||||
color: var(--text);
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.integration-tab.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -678,3 +678,16 @@ class PrintersPlugin(BasePlugin):
|
||||
return None
|
||||
# _computer_by_hostname returns the (Computer, Asset) pair.
|
||||
return row[1].assetid
|
||||
|
||||
def get_asset_type_descriptor(self):
|
||||
"""Return descriptor for printer types management in Asset Type Hub."""
|
||||
return {
|
||||
'assettype': 'printer',
|
||||
'subtype_endpoint': '/printers/types',
|
||||
'subtype_id_field': 'printertypeid',
|
||||
'subtype_label_field': 'printertype',
|
||||
'subtype_description_field': 'description',
|
||||
'subtype_color_field': 'color',
|
||||
'optional_tabs': [],
|
||||
'edit_permission': 'printers.manage',
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ export default [
|
||||
component: () => import('./views/DellWarrantySettings.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'warranty' }
|
||||
},
|
||||
{
|
||||
path: 'settings/integrations/dell',
|
||||
name: 'integrations-dell',
|
||||
component: () => import('./views/DellWarrantySettings.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'warranty' }
|
||||
},
|
||||
{
|
||||
path: 'warranties',
|
||||
name: 'warranties',
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Dell Warranty Lookup</h2>
|
||||
<h2>Integrations</h2>
|
||||
<p class="header-description">Configure external service connections and APIs</p>
|
||||
</div>
|
||||
|
||||
<div class="integration-tabs">
|
||||
<router-link
|
||||
:to="{ name: 'servicenow-settings' }"
|
||||
class="integration-tab"
|
||||
:class="{ active: $route.name === 'servicenow-settings' }"
|
||||
>
|
||||
ServiceNow
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'zabbix-settings' }"
|
||||
class="integration-tab"
|
||||
:class="{ active: $route.name === 'zabbix-settings' }"
|
||||
>
|
||||
Zabbix Supplies
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'dell-warranty-settings' }"
|
||||
class="integration-tab"
|
||||
:class="{ active: $route.name === 'dell-warranty-settings' }"
|
||||
>
|
||||
Dell Warranty
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
@@ -132,3 +157,39 @@ const dellMessage = computed(() => {
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-description {
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--text-light);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.integration-tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.integration-tab {
|
||||
padding: 0.75rem 1.5rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-light);
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.15s;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.integration-tab:hover {
|
||||
color: var(--text);
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.integration-tab.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user