harden CSV import reconciliation and preserve saved model selections
Default fuzzy reconcile threshold to 0 and refuse merges when digit runs differ so numbered labels like Bay 311 never bind to Bay 310. Keep edit forms from clearing saved model fields when category filtering hides untyped rows. Includes in-progress computers policy and endpoints command feature work.
This commit is contained in:
@@ -39,17 +39,56 @@ def _computer_models():
|
||||
return None
|
||||
|
||||
|
||||
def _token_is_collector(token):
|
||||
"""True when this managed token is a collector service token: scoped for
|
||||
collector.ingest AND its owner actually holds that permission (admins do)."""
|
||||
def _collector_scopes_for(pluginname=None):
|
||||
"""The scope names that authorize a collector request.
|
||||
|
||||
The generic scope `collector.ingest` authorizes every collector. A plugin
|
||||
MAY also mint a token scoped to `collector.ingest.<pluginname>` so that
|
||||
rotating/revoking it stops only that plugin's collector traffic (endpoints
|
||||
uses this for its regenerable agent kill switch). Both are accepted.
|
||||
"""
|
||||
scopes = {COLLECTOR_SCOPE}
|
||||
if pluginname:
|
||||
scopes.add(f'{COLLECTOR_SCOPE}.{pluginname}')
|
||||
return scopes
|
||||
|
||||
|
||||
def _env_fallback_disabled(pluginname=None):
|
||||
"""True when a plugin has switched OFF the bootstrap env-key fallback.
|
||||
|
||||
Once an operator provisions a UI-managed collector token, the plugin sets
|
||||
its `plugin.<pluginname>.envkeyfallback` setting to false so the old .env
|
||||
key stops authorizing. That turns key revocation into a real kill switch:
|
||||
rotating or revoking the managed token cannot be bypassed by a lingering env
|
||||
key. Absent
|
||||
setting = fallback stays ON (back-compat, so nothing breaks on upgrade).
|
||||
"""
|
||||
if not pluginname:
|
||||
return False
|
||||
from shopdb.core.models.setting import Setting
|
||||
key = f'plugin.{pluginname}.envkeyfallback'
|
||||
return Setting.get(key, default=True) is False
|
||||
|
||||
|
||||
def _token_is_collector(token, pluginname=None):
|
||||
"""True when this managed token is a collector service token: scoped for a
|
||||
collector scope AND its owner actually holds that permission (admins do).
|
||||
|
||||
Accepts the generic `collector.ingest` or, when pluginname is given, the
|
||||
plugin-specific `collector.ingest.<pluginname>` scope.
|
||||
"""
|
||||
from shopdb.core.models import User
|
||||
|
||||
scopelist = token.scopelist
|
||||
if not scopelist or COLLECTOR_SCOPE not in scopelist:
|
||||
if not scopelist:
|
||||
return False
|
||||
allowed = _collector_scopes_for(pluginname)
|
||||
held = [s for s in scopelist if s in allowed]
|
||||
if not held:
|
||||
return False
|
||||
user = db.session.get(User, token.userid)
|
||||
return (user is not None and user.isactive
|
||||
and user.haspermission(COLLECTOR_SCOPE))
|
||||
and any(user.haspermission(s) for s in held))
|
||||
|
||||
|
||||
def _collector_managed_token(api_key):
|
||||
@@ -73,24 +112,28 @@ def _collector_managed_token(api_key):
|
||||
return None
|
||||
|
||||
|
||||
def _check_collector_auth(expected_key):
|
||||
def _check_collector_auth(expected_key, pluginname=None):
|
||||
"""Authorize a collector request. Returns None when authorized, else the
|
||||
error_response to return.
|
||||
|
||||
Accepts a collector-scoped managed token (Bearer or X-API-Key) OR the env
|
||||
key via X-API-Key (legacy/bootstrap fallback). Env keys stay supported so
|
||||
nothing breaks; a valid managed token works even with no env key set.
|
||||
|
||||
When pluginname is given, a token scoped to collector.ingest.<pluginname>
|
||||
also authorizes (in addition to the generic collector.ingest scope).
|
||||
"""
|
||||
from shopdb.utils.apitoken_auth import touch_apitoken_lastused
|
||||
|
||||
api_key = request.headers.get('X-API-Key')
|
||||
|
||||
token = _collector_managed_token(api_key)
|
||||
if token is not None and _token_is_collector(token):
|
||||
if token is not None and _token_is_collector(token, pluginname):
|
||||
touch_apitoken_lastused(token)
|
||||
return None
|
||||
|
||||
if expected_key and api_key == expected_key:
|
||||
if expected_key and api_key == expected_key \
|
||||
and not _env_fallback_disabled(pluginname):
|
||||
return None
|
||||
|
||||
# Fail-closed only on pure server misconfiguration: no env key AND the
|
||||
@@ -200,7 +243,7 @@ def generic_collect(pluginname):
|
||||
|
||||
# Per-plugin env key (fallback) OR a collector-scoped managed token. Header
|
||||
# only (querystring fallback dropped, see require_api_key).
|
||||
error = _check_collector_auth(_plugin_api_key(pluginname))
|
||||
error = _check_collector_auth(_plugin_api_key(pluginname), pluginname)
|
||||
if error is not None:
|
||||
return error
|
||||
|
||||
|
||||
@@ -879,10 +879,10 @@ def build_default_settings():
|
||||
},
|
||||
{
|
||||
'key': 'asset_import_reconcile_threshold',
|
||||
'value': '0.85',
|
||||
'value': '0',
|
||||
'valuetype': 'string',
|
||||
'category': 'site',
|
||||
'description': 'Similarity (0 to 1) a fuzzy lookup match must reach for the asset importer to reconcile a near-miss name (e.g. "Dell Inc." to an existing "Dell") onto an existing vendor, model, type, status, business unit, or location. Reconciled matches are applied but reported for review. 0 disables reconciliation. Operating systems and asset rows are never reconciled.'
|
||||
'description': 'Similarity (0 to 1) a fuzzy lookup match must reach for the asset importer to reconcile a near-miss name (e.g. "Dell Inc." to an existing "Dell") onto an existing vendor, model, type, status, business unit, or location. Reconciled matches are applied but reported for review. 0 (the default) disables fuzzy reconciliation - names must match exactly after normalization. Labels whose digit runs differ (e.g. "Bay 310" vs "Bay 311") are never fuzzy-matched. Operating systems and asset rows are never reconciled.'
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user