mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:21:58 -06:00
Additional Device Merge Logic
This commit is contained in:
@@ -51,7 +51,7 @@ def register(
|
|||||||
return None
|
return None
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT d.guid, ds.site_id, s.name
|
SELECT d.guid, d.ssl_key_fingerprint, ds.site_id, s.name
|
||||||
FROM devices d
|
FROM devices d
|
||||||
LEFT JOIN device_sites ds ON ds.device_hostname = d.hostname
|
LEFT JOIN device_sites ds ON ds.device_hostname = d.hostname
|
||||||
LEFT JOIN sites s ON s.id = ds.site_id
|
LEFT JOIN sites s ON s.id = ds.site_id
|
||||||
@@ -63,19 +63,21 @@ def register(
|
|||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
existing_guid = normalize_guid(row[0])
|
existing_guid = normalize_guid(row[0])
|
||||||
|
existing_fingerprint = (row[1] or "").strip().lower()
|
||||||
pending_norm = normalize_guid(pending_guid)
|
pending_norm = normalize_guid(pending_guid)
|
||||||
if existing_guid and pending_norm and existing_guid == pending_norm:
|
if existing_guid and pending_norm and existing_guid == pending_norm:
|
||||||
return None
|
return None
|
||||||
site_id_raw = row[1]
|
site_id_raw = row[2]
|
||||||
site_id = None
|
site_id = None
|
||||||
if site_id_raw is not None:
|
if site_id_raw is not None:
|
||||||
try:
|
try:
|
||||||
site_id = int(site_id_raw)
|
site_id = int(site_id_raw)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
site_id = None
|
site_id = None
|
||||||
site_name = row[2] or ""
|
site_name = row[3] or ""
|
||||||
return {
|
return {
|
||||||
"guid": existing_guid or None,
|
"guid": existing_guid or None,
|
||||||
|
"ssl_key_fingerprint": existing_fingerprint or None,
|
||||||
"site_id": site_id,
|
"site_id": site_id,
|
||||||
"site_name": site_name,
|
"site_name": site_name,
|
||||||
}
|
}
|
||||||
@@ -275,14 +277,33 @@ def register(
|
|||||||
for row in rows:
|
for row in rows:
|
||||||
record_guid = row[2]
|
record_guid = row[2]
|
||||||
hostname = row[3]
|
hostname = row[3]
|
||||||
conflict = _hostname_conflict(cur, hostname, record_guid)
|
fingerprint_claimed = row[4]
|
||||||
|
claimed_fp_norm = (fingerprint_claimed or "").strip().lower()
|
||||||
|
conflict_raw = _hostname_conflict(cur, hostname, record_guid)
|
||||||
|
fingerprint_match = False
|
||||||
|
requires_prompt = False
|
||||||
|
conflict = None
|
||||||
|
if conflict_raw:
|
||||||
|
conflict_fp = (conflict_raw.get("ssl_key_fingerprint") or "").strip().lower()
|
||||||
|
fingerprint_match = bool(conflict_fp and claimed_fp_norm) and conflict_fp == claimed_fp_norm
|
||||||
|
requires_prompt = not fingerprint_match
|
||||||
|
conflict = {
|
||||||
|
**conflict_raw,
|
||||||
|
"fingerprint_match": fingerprint_match,
|
||||||
|
"requires_prompt": requires_prompt,
|
||||||
|
}
|
||||||
|
alternate_hostname = (
|
||||||
|
_suggest_alternate_hostname(cur, hostname, record_guid)
|
||||||
|
if conflict_raw and requires_prompt
|
||||||
|
else None
|
||||||
|
)
|
||||||
approvals.append(
|
approvals.append(
|
||||||
{
|
{
|
||||||
"id": row[0],
|
"id": row[0],
|
||||||
"approval_reference": row[1],
|
"approval_reference": row[1],
|
||||||
"guid": record_guid,
|
"guid": record_guid,
|
||||||
"hostname_claimed": hostname,
|
"hostname_claimed": hostname,
|
||||||
"ssl_key_fingerprint_claimed": row[4],
|
"ssl_key_fingerprint_claimed": fingerprint_claimed,
|
||||||
"enrollment_code_id": row[5],
|
"enrollment_code_id": row[5],
|
||||||
"status": row[6],
|
"status": row[6],
|
||||||
"client_nonce": row[7],
|
"client_nonce": row[7],
|
||||||
@@ -291,9 +312,9 @@ def register(
|
|||||||
"updated_at": row[10],
|
"updated_at": row[10],
|
||||||
"approved_by_user_id": row[11],
|
"approved_by_user_id": row[11],
|
||||||
"hostname_conflict": conflict,
|
"hostname_conflict": conflict,
|
||||||
"alternate_hostname": _suggest_alternate_hostname(cur, hostname, record_guid)
|
"alternate_hostname": alternate_hostname,
|
||||||
if conflict
|
"conflict_requires_prompt": requires_prompt,
|
||||||
else None,
|
"fingerprint_match": fingerprint_match,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
@@ -316,7 +337,10 @@ def register(
|
|||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT status
|
SELECT status,
|
||||||
|
guid,
|
||||||
|
hostname_claimed,
|
||||||
|
ssl_key_fingerprint_claimed
|
||||||
FROM device_approvals
|
FROM device_approvals
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
@@ -328,19 +352,48 @@ def register(
|
|||||||
existing_status = (row[0] or "").strip().lower()
|
existing_status = (row[0] or "").strip().lower()
|
||||||
if existing_status != "pending":
|
if existing_status != "pending":
|
||||||
return {"error": "approval_not_pending"}, 409
|
return {"error": "approval_not_pending"}, 409
|
||||||
|
stored_guid = row[1]
|
||||||
|
hostname_claimed = row[2]
|
||||||
|
fingerprint_claimed = (row[3] or "").strip().lower()
|
||||||
|
|
||||||
|
guid_effective = normalize_guid(guid) if guid else normalize_guid(stored_guid)
|
||||||
|
resolution_effective = (resolution.strip().lower() if isinstance(resolution, str) else None)
|
||||||
|
|
||||||
|
conflict = None
|
||||||
|
if status == "approved":
|
||||||
|
conflict = _hostname_conflict(cur, hostname_claimed, guid_effective)
|
||||||
|
if conflict:
|
||||||
|
conflict_fp = (conflict.get("ssl_key_fingerprint") or "").strip().lower()
|
||||||
|
fingerprint_match = bool(conflict_fp and fingerprint_claimed) and conflict_fp == fingerprint_claimed
|
||||||
|
if fingerprint_match:
|
||||||
|
guid_effective = conflict.get("guid") or guid_effective
|
||||||
|
if not resolution_effective:
|
||||||
|
resolution_effective = "auto_merge_fingerprint"
|
||||||
|
elif resolution_effective == "overwrite":
|
||||||
|
guid_effective = conflict.get("guid") or guid_effective
|
||||||
|
elif resolution_effective == "coexist":
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"error": "conflict_resolution_required",
|
||||||
|
"hostname": hostname_claimed,
|
||||||
|
}, 409
|
||||||
|
|
||||||
|
guid_to_store = guid_effective or normalize_guid(stored_guid) or None
|
||||||
|
|
||||||
approved_by = _lookup_user_id(cur, username) or username or "system"
|
approved_by = _lookup_user_id(cur, username) or username or "system"
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE device_approvals
|
UPDATE device_approvals
|
||||||
SET status = ?,
|
SET status = ?,
|
||||||
guid = COALESCE(?, guid),
|
guid = ?,
|
||||||
approved_by_user_id = ?,
|
approved_by_user_id = ?,
|
||||||
updated_at = ?
|
updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
status,
|
status,
|
||||||
guid,
|
guid_to_store,
|
||||||
approved_by,
|
approved_by,
|
||||||
_iso(_now()),
|
_iso(_now()),
|
||||||
approval_id,
|
approval_id,
|
||||||
@@ -349,11 +402,11 @@ def register(
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
resolution_note = f" ({resolution})" if resolution else ""
|
resolution_note = f" ({resolution_effective})" if resolution_effective else ""
|
||||||
log("server", f"device approval {approval_id} -> {status}{resolution_note} by {username}")
|
log("server", f"device approval {approval_id} -> {status}{resolution_note} by {username}")
|
||||||
payload: Dict[str, Any] = {"status": status}
|
payload: Dict[str, Any] = {"status": status}
|
||||||
if resolution:
|
if resolution_effective:
|
||||||
payload["conflict_resolution"] = resolution
|
payload["conflict_resolution"] = resolution_effective
|
||||||
return payload, 200
|
return payload, 200
|
||||||
|
|
||||||
@blueprint.route("/api/admin/device-approvals/<approval_id>/approve", methods=["POST"])
|
@blueprint.route("/api/admin/device-approvals/<approval_id>/approve", methods=["POST"])
|
||||||
|
|||||||
@@ -165,6 +165,20 @@ function DeviceApprovals() {
|
|||||||
});
|
});
|
||||||
const body = await resp.json().catch(() => ({}));
|
const body = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
|
if (resp.status === 409 && body.error === "conflict_resolution_required") {
|
||||||
|
const conflict = record.hostname_conflict;
|
||||||
|
const fallbackAlternate =
|
||||||
|
record.alternate_hostname ||
|
||||||
|
(record.hostname_claimed ? `${record.hostname_claimed}-1` : "");
|
||||||
|
if (conflict) {
|
||||||
|
setConflictPrompt({
|
||||||
|
record,
|
||||||
|
conflict,
|
||||||
|
alternate: fallbackAlternate || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
throw new Error(body.error || `Approval failed (${resp.status})`);
|
throw new Error(body.error || `Approval failed (${resp.status})`);
|
||||||
}
|
}
|
||||||
const appliedResolution = (body.conflict_resolution || payload.conflict_resolution || "").toLowerCase();
|
const appliedResolution = (body.conflict_resolution || payload.conflict_resolution || "").toLowerCase();
|
||||||
@@ -173,6 +187,8 @@ function DeviceApprovals() {
|
|||||||
successMessage = "Enrollment approved; existing device overwritten";
|
successMessage = "Enrollment approved; existing device overwritten";
|
||||||
} else if (appliedResolution === "coexist") {
|
} else if (appliedResolution === "coexist") {
|
||||||
successMessage = "Enrollment approved; devices will co-exist";
|
successMessage = "Enrollment approved; devices will co-exist";
|
||||||
|
} else if (appliedResolution === "auto_merge_fingerprint") {
|
||||||
|
successMessage = "Enrollment approved; device reconnected with its existing identity";
|
||||||
}
|
}
|
||||||
setFeedback({ type: "success", message: successMessage });
|
setFeedback({ type: "success", message: successMessage });
|
||||||
await loadApprovals();
|
await loadApprovals();
|
||||||
@@ -192,7 +208,8 @@ function DeviceApprovals() {
|
|||||||
if (status !== "pending") return;
|
if (status !== "pending") return;
|
||||||
const manualGuid = (guidInputs[record.id] || "").trim();
|
const manualGuid = (guidInputs[record.id] || "").trim();
|
||||||
const conflict = record.hostname_conflict;
|
const conflict = record.hostname_conflict;
|
||||||
if (conflict && !manualGuid) {
|
const requiresPrompt = Boolean(conflict?.requires_prompt ?? record.conflict_requires_prompt);
|
||||||
|
if (requiresPrompt && !manualGuid) {
|
||||||
const fallbackAlternate =
|
const fallbackAlternate =
|
||||||
record.alternate_hostname ||
|
record.alternate_hostname ||
|
||||||
(record.hostname_claimed ? `${record.hostname_claimed}-1` : "");
|
(record.hostname_claimed ? `${record.hostname_claimed}-1` : "");
|
||||||
|
|||||||
Reference in New Issue
Block a user