Let key-less PCs ask to enroll, and let an admin retry a trust-blocked PC
Found in the Windows VM re-test. - A PC installed without the shared key got 401 from POST /api/endpoints/enroll while the shared key was allowed, so no request was recorded and no admin could approve it. Enroll now never refuses: a valid shared key only decides auto-issue on a clean match; no key or an invalid key creates or updates the one pending request for that hostname and serial. An approved request still issues only to its own nonce. - A PC blocked with "server certificate not trusted by this PC" could never be queued again after trust was fixed, because the blocker comes from its last failed upgrade. Naming the PC in a wave now queues it when trust is its only blocker, and the response lists it under "retried"; "next n" still skips it. The Migration table lets such a PC be selected. - counts.pending on GET /enrollment counts pending requests, including ones for unknown hostnames; counts.pendingcomputers keeps the per-computer figure.
This commit is contained in:
@@ -28,7 +28,7 @@ import re
|
||||
from collections import Counter
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from flask import request
|
||||
from flask import current_app, request
|
||||
from flask_jwt_extended import current_user
|
||||
|
||||
from shopdb.api import (
|
||||
@@ -204,16 +204,24 @@ def _text_field(payload, name, maximum, required):
|
||||
def enroll_endpoint():
|
||||
"""A PC asks for its own credential.
|
||||
|
||||
Shared-key authenticated while plugin.endpoints.sharedkeyallowed is on.
|
||||
With it off the route takes no auth and only hands out a credential an
|
||||
admin already approved for this exact hostname, serial and nonce.
|
||||
Takes no auth. A PC installed without the shared key must still reach an
|
||||
admin, so a missing or invalid key never refuses the call: it records or
|
||||
updates the pending request for this hostname and serial. The shared key
|
||||
only decides auto-enroll: a valid key, while plugin.endpoints.sharedkeyallowed
|
||||
and plugin.endpoints.autoenroll are on, may issue on a clean match. Without
|
||||
one, only a credential an admin already approved for this exact hostname,
|
||||
serial and nonce is handed out.
|
||||
"""
|
||||
sharedkeyauthenticated = False
|
||||
if agentauth.shared_key_allowed():
|
||||
error = collector_authorized('endpoints')
|
||||
if error is not None:
|
||||
return error
|
||||
sharedkeyauthenticated = True
|
||||
presented = bool(request.headers.get('X-API-Key')
|
||||
or request.headers.get('Authorization'))
|
||||
if presented and agentauth.shared_key_allowed():
|
||||
if collector_authorized('endpoints') is None:
|
||||
sharedkeyauthenticated = True
|
||||
else:
|
||||
current_app.logger.warning(
|
||||
'endpoint enroll from %s presented an invalid API key; '
|
||||
'treated as unauthenticated', client_ip(request))
|
||||
|
||||
payload = request.get_json(silent=True)
|
||||
if not isinstance(payload, dict):
|
||||
@@ -505,11 +513,14 @@ def get_enrollment():
|
||||
if blocked else None),
|
||||
})
|
||||
|
||||
# Computer counts by state, except pending: that is every pending request,
|
||||
# including those naming no known computer, so it matches the request list.
|
||||
counts = Counter(row['state'] for row in rows)
|
||||
summary = {
|
||||
'enrolled': counts['enrolled'],
|
||||
'notenrolled': counts['notenrolled'] + counts['revoked'],
|
||||
'pending': counts['pending'],
|
||||
'pendingcomputers': counts['pending'],
|
||||
'pending': len(pending),
|
||||
}
|
||||
|
||||
state = (request.args.get('state') or '').strip().lower()
|
||||
@@ -771,6 +782,16 @@ def _is_tls_trust_failure(command):
|
||||
return any(marker in text for marker in _TLS_TRUST_FAILURES)
|
||||
|
||||
|
||||
def is_trust_retryable(blockers):
|
||||
"""True when nothing blocks the PC but, at most, the certificate trust failure.
|
||||
|
||||
The server only learns about trust from the PC's last failed download, so
|
||||
that blocker cannot clear by itself. An admin who fixed trust may queue the
|
||||
PC by id; a wave picking the next ready PCs never does.
|
||||
"""
|
||||
return all(blocker == BLOCKER_CERT_UNTRUSTED for blocker in blockers)
|
||||
|
||||
|
||||
def compute_readiness(assetids=None):
|
||||
"""Migration state for every active computer (or just assetids).
|
||||
|
||||
@@ -847,8 +868,8 @@ def compute_readiness(assetids=None):
|
||||
blockers.append(BLOCKER_CERT_UNTRUSTED)
|
||||
if not _version_below(agentversion, CREDENTIAL_AGENT_VERSION):
|
||||
blockers.append(BLOCKER_AWAITING_ENROLLMENT)
|
||||
if not blockers and 'run_script' not in ALLOWED_ACTIONS_FOR(
|
||||
assetid=assetid):
|
||||
if (is_trust_retryable(blockers)
|
||||
and 'run_script' not in ALLOWED_ACTIONS_FOR(assetid=assetid)):
|
||||
blockers.append(BLOCKER_POLICY)
|
||||
status = 'blocked' if blockers else 'ready'
|
||||
result.append({
|
||||
@@ -955,7 +976,9 @@ def create_migration_wave():
|
||||
|
||||
Body {"assetids": [...]} or {"next": n}. Readiness is re-checked per PC; the
|
||||
agent is packaged once for the whole wave, then each PC is queued through
|
||||
the same path as POST /<asset>/agentupgrade.
|
||||
the same path as POST /<asset>/agentupgrade. An explicit id whose only
|
||||
blocker is the certificate trust failure is queued too and listed in
|
||||
retried: the admin is saying trust was fixed.
|
||||
"""
|
||||
from .routes import prepare_agent_upgrade, queue_prepared_agent_upgrade
|
||||
|
||||
@@ -987,10 +1010,15 @@ def create_migration_wave():
|
||||
readiness = {row['assetid']: row for row in compute_readiness(candidates)}
|
||||
skipped = []
|
||||
ready = []
|
||||
retrying = set()
|
||||
for assetid in candidates:
|
||||
row = readiness.get(assetid)
|
||||
if row is None:
|
||||
skipped.append({'assetid': assetid, 'reason': 'not a computer'})
|
||||
elif (requested is not None and row['state'] == 'blocked'
|
||||
and row['blockers'] and is_trust_retryable(row['blockers'])):
|
||||
retrying.add(assetid)
|
||||
ready.append(assetid)
|
||||
elif row['state'] != 'ready':
|
||||
reason = ', '.join(row['blockers']) if row['blockers'] else row['state']
|
||||
skipped.append({'assetid': assetid, 'reason': reason})
|
||||
@@ -1027,8 +1055,10 @@ def create_migration_wave():
|
||||
.format(len(queued), wave.assetids))
|
||||
db.session.commit()
|
||||
|
||||
retried = [assetid for assetid in queued if assetid in retrying]
|
||||
return success_response(
|
||||
{'waveid': waveid, 'queued': queued, 'skipped': skipped},
|
||||
{'waveid': waveid, 'queued': queued, 'skipped': skipped,
|
||||
'retried': retried},
|
||||
message='{} PC(s) queued'.format(len(queued)),
|
||||
http_code=201 if queued else 200)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user