Act on the wj architectural review: races, limits, pool and contract gaps
Findings F2, F4, F5, F6, F7, F8, F9, F10, F11 and F13 from wj-architectural-review.md. F1/F1a (the non-sargable LOWER pattern) and F3 (the warranty sync holding a pooled connection) are being handled separately and are untouched here. - A collector check-in that raced another for the same PC lost everything: the unique index on the application fingerprint raised, nothing caught it, and the whole check-in rolled back, discarding state, events and hardware, answering the agent with form copy meant for a person. The insert now runs in a savepoint, reconciles what the other transaction already wrote, one row per savepoint so a second collision skips a row rather than the batch, and the check-in commits with a warning. This is the shape the delete side of the same function already had. - Enrollment is deliberately unauthenticated, so it now carries a per-address rate limit and a cap on pending requests, and the cap is a setting an admin can see and change. A flood can no longer bury the approval queue or grow the table without bound. - The fleet alert poll no longer formats debug strings on every request. - BusinessUnit is on the contract surface (0.34.0), so the endpoints plugin stops reaching into a core table with raw SQL. - A test walks every asset type descriptor and proves the endpoint it names really refuses a caller without the declared permission. - The plugin settings routes now need a permission; they were telling anyone which modules a site runs and where its components live. - The connection pool is sized explicitly with a short timeout, and the arithmetic against the server's connection ceiling is written next to the worker count instead of being left to defaults nobody chose. - Fleet alerts carry both the correct lowercase keys and the old camelCase pair. Trays older than 1.7.0 match keys case-sensitively and would have gone quiet on every PC not yet upgraded; the old pair goes when they are. - The dev server proxy points at localhost again, with environment variables for the remote-box workflow, and one developer's address is out of the server certificate page.
This commit is contained in:
@@ -3957,7 +3957,7 @@ def _endpoint_scope_facts(hostname):
|
||||
matchable.
|
||||
"""
|
||||
from plugins.computers.models import Computer, ComputerType
|
||||
from shopdb.api import Location, AssetType
|
||||
from shopdb.api import BusinessUnit, Location, AssetType
|
||||
|
||||
row = (db.session.query(Computer.hostname, AssetType.assettype,
|
||||
ComputerType.computertype, Location.locationname,
|
||||
@@ -3977,15 +3977,13 @@ def _endpoint_scope_facts(hostname):
|
||||
facts = {'hostname': hostname_value, 'assettype': assettype,
|
||||
'computertype': computertype, 'location': locationname}
|
||||
|
||||
# BusinessUnit is not on the plugin contract surface (shopdb.api), so resolve
|
||||
# its name by table rather than importing the model and breaking the import
|
||||
# contract the way the mailer import did.
|
||||
# BusinessUnit joined the contract surface at 0.34.0 precisely so this is a
|
||||
# primary-key get through the model rather than db.text() against a core
|
||||
# table. A raw query here respected the letter of ADR-002 and defeated its
|
||||
# point: hidden coupling to the core schema that no contract bump catches.
|
||||
if businessunitid is not None:
|
||||
name = db.session.execute(
|
||||
db.text('SELECT businessunit FROM businessunits '
|
||||
'WHERE businessunitid = :bid'),
|
||||
{'bid': businessunitid}).scalar()
|
||||
facts['businessunit'] = name
|
||||
unit = db.session.get(BusinessUnit, businessunitid)
|
||||
facts['businessunit'] = unit.businessunit if unit is not None else None
|
||||
|
||||
return {k: str(v).strip().lower() for k, v in facts.items()
|
||||
if v is not None and str(v).strip()}
|
||||
@@ -4099,35 +4097,39 @@ def get_fleet_alerts():
|
||||
# computer row means fleet-wide rather than a crash.
|
||||
facts = _endpoint_scope_facts(hostname) or {}
|
||||
|
||||
current_app.logger.debug(
|
||||
f'[fleet_alerts] hostname={hostname} asset_id={asset_id} '
|
||||
f'facts={facts} query_results={len(results)}')
|
||||
|
||||
alerts = []
|
||||
for notification, ntype in results:
|
||||
matches = _alert_targets_endpoint(notification.alertscope, facts)
|
||||
current_app.logger.debug(
|
||||
f'[fleet_alerts] notification_id={notification.notificationid} '
|
||||
f'scope={notification.alertscope} matches={matches}')
|
||||
if not matches:
|
||||
if not _alert_targets_endpoint(notification.alertscope, facts):
|
||||
continue
|
||||
|
||||
title = notification.notification[:100] if notification.notification else ''
|
||||
if '\n' in title:
|
||||
title = title.split('\n')[0][:100] or '(no title)'
|
||||
|
||||
alert_data = {
|
||||
'notificationId': notification.notificationid,
|
||||
alerts.append({
|
||||
'notificationid': notification.notificationid,
|
||||
'type': ntype.typename,
|
||||
'typeColor': ntype.typecolor or _DEFAULT_ALERT_TYPE_COLOR,
|
||||
'typecolor': ntype.typecolor or _DEFAULT_ALERT_TYPE_COLOR,
|
||||
'title': title,
|
||||
'message': notification.notification or '',
|
||||
'timeout': notification.alerttimeout,
|
||||
}
|
||||
alerts.append(alert_data)
|
||||
current_app.logger.debug(f'[fleet_alerts] added alert: {alert_data}')
|
||||
# TRANSITION, remove once no tray older than 1.7.0 is left in the
|
||||
# fleet: a deployed 1.6.x tray fetches this feed itself and
|
||||
# deserializes case-sensitively, so dropping these two keys makes
|
||||
# every alert vanish on those PCs with no error anywhere. The
|
||||
# lowercase pair above is the contract; these two are a bridge.
|
||||
# Any NEW field is lowercase only.
|
||||
'notificationId': notification.notificationid,
|
||||
'typeColor': ntype.typecolor or _DEFAULT_ALERT_TYPE_COLOR,
|
||||
})
|
||||
|
||||
current_app.logger.debug(f'[fleet_alerts] returning {len(alerts)} alerts')
|
||||
# ONE line, lazily formatted. This runs on every poll from every endpoint,
|
||||
# so an f-string here is formatting work paid at every log level (the cost
|
||||
# HOTFIX 1ed0c4d removed from the collector check-in); per-notification and
|
||||
# per-alert lines multiplied it by the fleet alert count.
|
||||
current_app.logger.debug(
|
||||
'[fleet_alerts] hostname=%s assetid=%s candidates=%d returned=%d',
|
||||
hostname, asset_id, len(results), len(alerts))
|
||||
return success_response(alerts)
|
||||
|
||||
|
||||
@@ -4679,8 +4681,8 @@ def generate_server_certificate():
|
||||
|
||||
Expects JSON body:
|
||||
{
|
||||
"cn": "localhost",
|
||||
"san": "DNS:localhost,IP:192.168.100.1",
|
||||
"cn": "<hostname or IP agents connect to>",
|
||||
"san": "DNS:<hostname>,IP:<address>",
|
||||
"days": 365
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user