diff --git a/docs/API_DEVICE_FIELD_LOCK.md b/docs/API_DEVICE_FIELD_LOCK.md index 3c932f10..a6f076ab 100644 --- a/docs/API_DEVICE_FIELD_LOCK.md +++ b/docs/API_DEVICE_FIELD_LOCK.md @@ -7,10 +7,10 @@ The Device Field Lock/Unlock feature allows users to lock specific device fields ## Concepts ### Tracked Fields + Only certain device fields support locking. These are the fields that can be modified by both plugins and users: -- `devMac` - Device MAC address -- `devName` - Device name/hostname -- `devLastIP` - Last known IP address + +- `devName` - Device name/hostname - `devVendor` - Device vendor/manufacturer - `devFQDN` - Fully qualified domain name - `devSSID` - Network SSID @@ -20,14 +20,18 @@ Only certain device fields support locking. These are the fields that can be mod - `devVlan` - VLAN identifier ### Field Source Tracking + Every tracked field has an associated `*Source` field that indicates where the current value originated: + - `NEWDEV` - Created via the UI as a new device - `USER` - Manually edited by a user - `LOCKED` - Field is locked; prevents any plugin overwrites - Plugin name (e.g., `UNIFIAPI`, `PIHOLE`) - Last updated by this plugin ### Locking Mechanism + When a field is **locked**, its source is set to `LOCKED`. This prevents plugin overwrites based on the authorization logic: + 1. Plugin wants to update field 2. Authoritative handler checks field's `*Source` value 3. If `*Source` == `LOCKED`, plugin update is rejected @@ -38,6 +42,7 @@ When a field is **unlocked**, its source is set to `NEWDEV`, allowing plugins to ## Endpoints ### Lock or Unlock a Field + ``` POST /device/{mac}/field/lock Authorization: Bearer {API_TOKEN} @@ -134,6 +139,7 @@ The Device Edit form displays lock/unlock buttons for all tracked fields: 3. **Source Indicator**: Shows current field source (USER, LOCKED, NEWDEV, or plugin name) ### Source Indicator Colors + - Red (USER): Field was manually edited by a user - Orange (LOCKED): Field is locked and protected from overwrites - Gray (NEWDEV/Plugin): Field value came from automatic discovery @@ -141,6 +147,7 @@ The Device Edit form displays lock/unlock buttons for all tracked fields: ## UI Workflow ### Locking a Field via UI + 1. Navigate to Device Details 2. Find the field you want to protect 3. Click the lock button (πŸ”’) next to the field @@ -148,6 +155,7 @@ The Device Edit form displays lock/unlock buttons for all tracked fields: 5. Field is now protected from plugin overwrites ### Unlocking a Field via UI + 1. Find the locked field (button shows πŸ”“) 2. Click the unlock button 3. Button changes back to lock (πŸ”’) and source resets to NEWDEV @@ -167,59 +175,16 @@ The lock/unlock feature is implemented in: - **Data Model**: `/server/models/device_instance.py` - Authorization checks in `setDeviceData()` - **Database**: Devices table with `*Source` columns tracking field origins -### Frontend Logic -The lock/unlock UI is implemented in: -- **Device Edit Form**: `/front/deviceDetailsEdit.php` - - Form rendering with lock/unlock buttons - - JavaScript function `toggleFieldLock()` for API calls - - Source indicator display -- **Styling**: `/front/css/app.css` - Lock button and source indicator styles - ### Authorization Handler + The authoritative field update logic prevents plugin overwrites: + 1. Plugin provides new value for field via plugin config `SET_ALWAYS`/`SET_EMPTY` 2. Authoritative handler (in DeviceInstance) checks `{field}Source` value 3. If source is `LOCKED` or `USER`, plugin update is rejected 4. If source is `NEWDEV` or plugin name, plugin update is accepted -## Best Practices - -### When to Lock Fields -- Device names that you've customized -- Static IP addresses or important identifiers -- Device vendor information you've corrected -- Fields prone to incorrect plugin updates - -### When to Keep Unlocked -- Fields that plugins actively maintain (MAC, IP address) -- Fields you want auto-updated by discovery plugins -- Fields that may change frequently in your network - -### Bulk Operations -The field lock/unlock feature is currently per-device. For bulk locking: -1. Use Multi-Edit to update device fields -2. Then use individual lock operations via API script -3. Or contact support for bulk lock endpoint - -## Troubleshooting - -### Lock Button Not Visible -- Device must be saved/created first (not "new" device) -- Field must be one of the 10 tracked fields -- Check browser console for JavaScript errors - -### Lock Operation Failed -- Verify API token is valid -- Check device MAC address is correct -- Ensure device exists in database - -### Field Still Updating After Lock -- Verify lock was successful (check API response) -- Reload device details page -- Check plugin logs to see if plugin is providing the field -- Look for authorization errors in NetAlertX logs - ## See Also -- [API Device Endpoints Documentation](API_DEVICE.md) -- [Authoritative Field Updates System](../docs/PLUGINS_DEV.md#authoritative-fields) -- [Plugin Configuration Reference](../docs/PLUGINS_DEV_CONFIG.md) +- [API Device Endpoints Documentation](./API_DEVICE.md) +- [Authoritative Field Updates System](./PLUGINS_DEV.md#authoritative-fields) +- [Plugin Configuration Reference](./PLUGINS_DEV_CONFIG.md) diff --git a/docs/PLUGINS_DEV_CONFIG.md b/docs/PLUGINS_DEV_CONFIG.md index c4eda793..bdc7efcc 100755 --- a/docs/PLUGINS_DEV_CONFIG.md +++ b/docs/PLUGINS_DEV_CONFIG.md @@ -187,20 +187,20 @@ For tracked fields (devMac, devName, devLastIP, devVendor, devFQDN, devSSID, dev Controls whether a plugin field is enabled: -- `"1"` - Plugin can always overwrite this field when authorized (subject to source-based permissions) -- `"0"` - Plugin doesn't use this field +- `["devName", "devLastIP"]` - Plugin can always overwrite this field when authorized (subject to source-based permissions) + +**Authorization logic:** Even with a field listed in `SET_ALWAYS`, the plugin respects source-based permissions: -**Authorization logic:** Even with `SET_ALWAYS: "1"`, the plugin respects source-based permissions: - Cannot overwrite `USER` source (user manually edited) - Cannot overwrite `LOCKED` source (user locked field) - Can overwrite `NEWDEV` or plugin-owned sources (if plugin has SET_ALWAYS enabled) +- Will update plugin-owned sources if value the same **Example in config.json:** + ```json { - "setKey": "NEWDEV_devName", - "displayName": "Device Name", - "SET_ALWAYS": "1" + "SET_ALWAYS": ["devName", "devLastIP"] } ``` @@ -210,50 +210,18 @@ Controls whether a plugin field is enabled: Restricts when a plugin can update a field: -- `"1"` - Overwrite only if current value is empty OR source is NEWDEV (conservative mode) -- `"0"` - No extra restriction; respect authorization logic (default) +- `"SET_EMPTY": ["devName", "devLastIP"]` - Overwrite these fields only if current value is empty OR source is `NEWDEV` -**Use case:** Some plugins discover optional enrichment data (like vendor/hostname) that shouldn't override user-set or existing values. Use `SET_EMPTY: "1"` to be less aggressive. +**Use case:** Some plugins discover optional enrichment data (like vendor/hostname) that shouldn't override user-set or existing values. Use `SET_EMPTY` to be less aggressive. -**Example in config.json:** -```json -{ - "setKey": "NEWDEV_devVendor", - "displayName": "Device Vendor", - "SET_ALWAYS": "1", - "SET_EMPTY": "1" -} -``` ### Authorization Decision Flow 1. **Source check:** Is field LOCKED or USER? β†’ REJECT (protected) -2. **SET_ALWAYS check:** Is SET_ALWAYS enabled for this plugin+field? β†’ YES: ALLOW (can overwrite empty values, NEWDEV, plugin sources, etc.) | NO: Continue to step 3 -3. **SET_EMPTY check:** Is SET_EMPTY enabled AND field non-empty+non-NEWDEV? β†’ REJECT +2. **Field in SET_ALWAYS check:** Is SET_ALWAYS enabled for this plugin+field? β†’ YES: ALLOW (can overwrite empty values, NEWDEV, plugin sources, etc.) | NO: Continue to step 3 +3. **Field in SET_EMPTY check:** Is SET_EMPTY enabled AND field non-empty+non-NEWDEV? β†’ REJECT 4. **Default behavior:** Allow overwrite if field empty or NEWDEV source -### Plugin Field Mappings Reference - -This table shows all device discovery and enrichment plugins and their tracked field configuration: - -| Plugin | Tracked Fields | Behavior | -|--------|---|---| -| ARPSCAN | devMac, devLastIP | SET_ALWAYS for both | -| IPNEIGH | devMac, devLastIP | SET_ALWAYS for both | -| DHCPLSS | devMac, devLastIP | SET_ALWAYS for both | -| ASUSWRT | devMac, devLastIP | SET_ALWAYS for both | -| LUCIRPC | devMac, devLastIP | SET_ALWAYS for both | -| PIHOLE | devMac, devLastIP, devName, devVendor | SET_ALWAYS for MAC/IP | -| PIHOLEAPI | devMac, devLastIP, devName, devVendor | SET_ALWAYS for MAC/IP, SET_EMPTY for name/vendor | -| NBTSCAN | devName | SET_ALWAYS | -| DIGSCAN | devName, devFQDN | SET_ALWAYS | -| NSLOOKUP | devName, devFQDN | SET_ALWAYS | -| AVAHISCAN | devName | SET_ALWAYS | -| VNDRPDT | devMac, devVendor | SET_ALWAYS for both | -| SNMPDSC | devMac, devLastIP | SET_ALWAYS for both | -| UNIFIMP | devMac, devLastIP, devName, devVendor, devSSID, devParentMAC, devParentPort | SET_ALWAYS for MAC/IP | -| UNIFIAPI | devMac, devLastIP, devName, devParentMAC | SET_ALWAYS for MAC/IP | - **Note:** Check each plugin's `config.json` manifest for its specific SET_ALWAYS/SET_EMPTY configuration. --- diff --git a/docs/QUICK_REFERENCE_FIELD_LOCK.md b/docs/QUICK_REFERENCE_FIELD_LOCK.md index 3e823e29..c2f28397 100644 --- a/docs/QUICK_REFERENCE_FIELD_LOCK.md +++ b/docs/QUICK_REFERENCE_FIELD_LOCK.md @@ -10,16 +10,14 @@ The device field lock/unlock system allows you to protect specific device fields These are the ONLY fields that can be locked: -1. devMac - Device MAC address -2. devName - Device hostname/alias -3. devLastIP - Last known IP address -4. devVendor - Device manufacturer -5. devFQDN - Fully qualified domain name -6. devSSID - WiFi network name -7. devParentMAC - Parent/gateway MAC -8. devParentPort - Parent device port -9. devParentRelType - Relationship type (e.g., "gateway") -10. devVlan - VLAN identifier +- devName - Device hostname/alias +- devVendor - Device manufacturer +- devFQDN - Fully qualified domain name +- devSSID - WiFi network name +- devParentMAC - Parent/gateway MAC +- devParentPort - Parent device port +- devParentRelType - Relationship type (e.g., "gateway") +- devVlan - VLAN identifier ## Source Values Explained @@ -27,10 +25,10 @@ Each locked field has a "source" indicator that shows you why the value is prote | Indicator | Meaning | Can It Change? | |-----------|---------|---| -| πŸ”’ **LOCKED** (red badge) | You locked this field | No, until you unlock it | -| ✏️ **USER** (orange badge) | You edited this field | No, plugins can't overwrite | -| πŸ“‘ **NEWDEV** (gray badge) | Default/unset value | Yes, plugins can update | -| πŸ“‘ **Plugin name** (gray badge) | Last updated by a plugin (e.g., UNIFIAPI) | Yes, plugins can update | +| πŸ”’ **LOCKED** | You locked this field | No, until you unlock it | +| ✏️ **USER** | You edited this field | No, plugins can't overwrite | +| πŸ“‘ **NEWDEV** | Default/unset value | Yes, plugins can update | +| πŸ“‘ **Plugin name** | Last updated by a plugin (e.g., UNIFIAPI) | Yes, plugins can update if field in SET_ALWAYS | ## How to Use @@ -39,15 +37,15 @@ Each locked field has a "source" indicator that shows you why the value is prote 1. Navigate to **Device Details** for the device 2. Find the field you want to protect (e.g., device name) 3. Click the **lock button** (πŸ”’) next to the field -4. The button changes to **unlock** (πŸ”“) and turns red +4. The button changes to **unlock** (πŸ”“) 5. That field is now protected ### Unlock a Field (Allow Plugin Updates) 1. Go to **Device Details** -2. Find the locked field (shows πŸ”“ in red) +2. Find the locked field (shows πŸ”“) 3. Click the **unlock button** (πŸ”“) -4. The button changes back to **lock** (πŸ”’) and turns gray +4. The button changes back to **lock** (πŸ”’) 5. Plugins can now update that field again ## Common Scenarios @@ -77,9 +75,9 @@ Each locked field has a "source" indicator that shows you why the value is prote - βœ… Your custom value is kept - βœ… Future plugin scans won't overwrite it -- βœ… You can still manually edit it anytime +- βœ… You can still manually edit it anytime after unlocking - βœ… Lock persists across plugin runs -- βœ… Other users can see it's locked (red indicator) +- βœ… Other users can see it's locked ## What Happens When You Unlock a Field @@ -92,7 +90,7 @@ Each locked field has a "source" indicator that shows you why the value is prote | Message | What It Means | What to Do | |---------|--------------|-----------| -| "Field cannot be locked" | You tried to lock a field that doesn't support locking | Only lock the 10 fields listed above | +| "Field cannot be locked" | You tried to lock a field that doesn't support locking | Only lock the fields listed above | | "Device not found" | The device MAC address doesn't exist | Verify the device hasn't been deleted | | Lock button doesn't work | Network or permission issue | Refresh the page and try again | | Unexpected field changed | Field might have been unlocked | Check if field shows unlock icon (πŸ”“) | @@ -121,7 +119,7 @@ Each locked field has a "source" indicator that shows you why the value is prote ## Troubleshooting **Lock button not appearing:** -- Confirm the field is one of the 10 tracked fields (see list above) +- Confirm the field is one of the tracked fields (see list above) - Confirm the device is already saved (new devices don't show lock buttons) - Refresh the page @@ -132,10 +130,10 @@ Each locked field has a "source" indicator that shows you why the value is prote - Try again in a few seconds **Field still changes after locking:** -- Double-check the lock icon shows (red indicator) +- Double-check the lock icon shows - Reload the pageβ€”the change might be a display issue - Check if you accidentally unlocked it -- Contact support if it persists +- pen an issue if it persists ## For More Information diff --git a/server/scan/session_events.py b/server/scan/session_events.py index bdf8e03f..e0ae162f 100755 --- a/server/scan/session_events.py +++ b/server/scan/session_events.py @@ -102,7 +102,7 @@ def process_scan(db): # Clear current scan as processed # πŸ› CurrentScan DEBUG: comment out below when debugging to keep the CurrentScan table after restarts/scan finishes - # db.sql.execute("DELETE FROM CurrentScan") + db.sql.execute("DELETE FROM CurrentScan") # Commit changes db.commitDB() diff --git a/test/authoritative_fields/test_authoritative_handler.py b/test/authoritative_fields/test_authoritative_handler.py index e30f24a8..f22d53fb 100644 --- a/test/authoritative_fields/test_authoritative_handler.py +++ b/test/authoritative_fields/test_authoritative_handler.py @@ -15,61 +15,61 @@ class TestCanOverwriteField: def test_user_source_prevents_overwrite(self): """USER source should prevent any overwrite.""" assert not can_overwrite_field( - "devName", "USER", "UNIFIAPI", {"set_always": [], "set_empty": []}, "NewName" + "devName", "OldName", "USER", "UNIFIAPI", {"set_always": [], "set_empty": []}, "NewName" ) def test_locked_source_prevents_overwrite(self): """LOCKED source should prevent any overwrite.""" assert not can_overwrite_field( - "devName", "LOCKED", "ARPSCAN", {"set_always": [], "set_empty": []}, "NewName" + "devName", "OldName", "LOCKED", "ARPSCAN", {"set_always": [], "set_empty": []}, "NewName" ) def test_empty_value_prevents_overwrite(self): """Empty/None values should prevent overwrite.""" assert not can_overwrite_field( - "devName", "", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, "" + "devName", "OldName", "", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, "" ) assert not can_overwrite_field( - "devName", "", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, None + "devName", "OldName", "", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, None ) def test_set_always_allows_overwrite(self): """SET_ALWAYS should allow overwrite regardless of current source.""" assert can_overwrite_field( - "devName", "ARPSCAN", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, "NewName" + "devName", "OldName", "ARPSCAN", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, "NewName" ) assert can_overwrite_field( - "devName", "NEWDEV", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, "NewName" + "devName", "", "NEWDEV", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, "NewName" ) def test_set_empty_allows_overwrite_only_when_empty(self): """SET_EMPTY should allow overwrite only if field is empty or NEWDEV.""" assert can_overwrite_field( - "devName", "", "UNIFIAPI", {"set_always": [], "set_empty": ["devName"]}, "NewName" + "devName", "", "", "UNIFIAPI", {"set_always": [], "set_empty": ["devName"]}, "NewName" ) assert can_overwrite_field( - "devName", "NEWDEV", "UNIFIAPI", {"set_always": [], "set_empty": ["devName"]}, "NewName" + "devName", "", "NEWDEV", "UNIFIAPI", {"set_always": [], "set_empty": ["devName"]}, "NewName" ) assert not can_overwrite_field( - "devName", "ARPSCAN", "UNIFIAPI", {"set_always": [], "set_empty": ["devName"]}, "NewName" + "devName", "OldName", "ARPSCAN", "UNIFIAPI", {"set_always": [], "set_empty": ["devName"]}, "NewName" ) def test_default_behavior_overwrites_empty_fields(self): """Without SET_ALWAYS/SET_EMPTY, should overwrite only empty fields.""" assert can_overwrite_field( - "devName", "", "UNIFIAPI", {"set_always": [], "set_empty": []}, "NewName" + "devName", "", "", "UNIFIAPI", {"set_always": [], "set_empty": []}, "NewName" ) assert can_overwrite_field( - "devName", "NEWDEV", "UNIFIAPI", {"set_always": [], "set_empty": []}, "NewName" + "devName", "", "NEWDEV", "UNIFIAPI", {"set_always": [], "set_empty": []}, "NewName" ) assert not can_overwrite_field( - "devName", "ARPSCAN", "UNIFIAPI", {"set_always": [], "set_empty": []}, "NewName" + "devName", "OldName", "ARPSCAN", "UNIFIAPI", {"set_always": [], "set_empty": []}, "NewName" ) def test_whitespace_value_treated_as_empty(self): """Whitespace-only values should be treated as empty.""" assert not can_overwrite_field( - "devName", "", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, " " + "devName", "OldName", "", "UNIFIAPI", {"set_always": ["devName"], "set_empty": []}, " " ) diff --git a/test/authoritative_fields/test_device_field_lock.py b/test/authoritative_fields/test_device_field_lock.py index c71dda7a..8cdcdbc8 100644 --- a/test/authoritative_fields/test_device_field_lock.py +++ b/test/authoritative_fields/test_device_field_lock.py @@ -355,6 +355,7 @@ class TestFieldLockIntegration: proposed_value = "Plugin Name" can_overwrite = can_overwrite_field( "devName", + device_data.get("devName"), device_data.get("devNameSource"), plugin_prefix, plugin_settings,