Skip to content

Users: get_users + set_password (223), plaintext, verified live - #14

Merged
widgetii merged 2 commits into
masterfrom
setters-users
Sep 22, 2026
Merged

widgetii merged 2 commits into
masterfrom
setters-users

Conversation

@widgetii

Copy link
Copy Markdown
Member

What

User-account access over code 223 = SystemConfig/UserConfig, captured from AjDevTools "Batch Set Password" and verified live on an MTF45-4G_AF.

  • set_password(password, username="admin", group=..., status=..., confirm=True) — the tool sends the plaintext password; the device computes the stored EncryptPwd (no client-side hashing, so the EncryptPwd cipher is not needed). Body: <UserConfig><Account Username=".." Password="<plaintext>" Group=".." Status=".." /></UserConfig>. Verified live: changed the admin password, re-authenticated with the new one (old rejected), then reverted to 123456.
  • get_users() — reads the accounts (Username/Group/Status/EncryptPwd) from the config download (read-only).

Safety

set_password changes the login for every service (binary control, ONVIF, web), so it is confirm=True-gated. The docstring says so.

Changes

  • anjoy/comm.py: get_users, set_password.
  • anjoy/const.py: CFG_USER="223".
  • tests/test_comm.py: plaintext frame, confirm gate, XML-escaping of special chars, get_users parsing (91 tests pass).
  • docs/devices.md: the captured 223 body + the plaintext/EncryptPwd note.

Captured from AjDevTools "Batch Set Password" and confirmed live on MTF45-4G_AF:
code 223 = SystemConfig/UserConfig. The tool sends the password in PLAINTEXT
(<UserConfig><Account Username=".." Password="<plaintext>" Group=".." Status=".."
/></UserConfig>) and the device computes the stored EncryptPwd — no client-side
hashing, so the EncryptPwd cipher is not needed to set a password.

- set_password(password, username="admin", group=..., status=..., confirm=True):
  verified live by changing the admin password, re-authenticating with it, then
  reverting to 123456.
- get_users(): parses the accounts (Username/Group/Status/EncryptPwd) from the
  config download (read-only).

set_password changes the login for every service (binary control, ONVIF, web),
so it is confirm-gated. const.CFG_USER="223". Tests: plaintext frame, confirm
gate, XML-escaping of special chars, get_users parsing. Docs updated.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add user listing and confirm-gated password updates

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Add read-only user enumeration from full device configuration downloads.
• Add confirm-gated password writes using verified plaintext code 223 frames.
• Document protocol behavior and test framing, parsing, safety, and XML escaping.
Diagram

sequenceDiagram
    actor Caller
    participant Client as Anjoy Client
    participant Device as Camera
    participant Parser as XML Parser
    alt Read users
        Caller->>Client: get_users()
        Client->>Device: Download configuration
        Device-->>Client: Full config XML
        Client->>Parser: Parse UserConfig
        Parser-->>Client: Account attributes
        Client-->>Caller: User list
    else Set password
        Caller->>Client: set_password(confirm)
        Client->>Device: Code 223 plaintext
        Device-->>Client: Acknowledge write
        Client-->>Caller: Write result
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Read account metadata before writing
  • ➕ Preserves the selected account's existing group and status by default.
  • ➕ Reduces accidental privilege or activation changes for non-admin accounts.
  • ➖ Adds a full configuration download before every password update.
  • ➖ Introduces an additional failure point and a read-write race.
  • ➖ Deviates from the simplest vendor-captured request flow.
2. Require explicit metadata for non-admin users
  • ➕ Avoids silently applying Administrator and Enable defaults to another account.
  • ➕ Retains the direct, verified code 223 write path without an extra download.
  • ➖ Makes the API conditional and less convenient.
  • ➖ Pushes responsibility for discovering current metadata onto callers.

Recommendation: Keep the verified plaintext code 223 flow and confirmation gate; client-side EncryptPwd generation is unnecessary and unsupported by the observed protocol. If non-admin password updates are intended, preserve current group/status through a read-before-write step or require those fields explicitly, since the existing defaults could unintentionally promote or enable another account.

Files changed (4) +95 / -0

Enhancement (1) +34 / -0
comm.pyAdd user enumeration and password-setting APIs +34/-0

Add user enumeration and password-setting APIs

• Adds get_users() to parse UserConfig accounts from the full configuration download. Adds confirm-gated set_password() using plaintext, XML-escaped account attributes and configuration code 223.

anjoy/comm.py

Tests (1) +44 / -0
test_comm.pyCover user parsing and safe password writes +44/-0

Cover user parsing and safe password writes

• Tests multiple-account parsing, plaintext code 223 framing, the mandatory confirmation gate, and XML escaping for special password characters.

tests/test_comm.py

Documentation (1) +16 / -0
devices.mdDocument the captured user configuration protocol +16/-0

Document the captured user configuration protocol

• Documents code 223, the plaintext password payload, device-computed EncryptPwd behavior, live verification, and the cross-service impact of credential changes.

docs/devices.md

Other (1) +1 / -0
const.pyRegister user configuration code 223 +1/-0

Register user configuration code 223

• Defines CFG_USER for SystemConfig/UserConfig account and password operations.

anjoy/const.py

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Guest password resets grant admin rights ✓ Resolved 🐞 Bug ⛨ Security
Description
set_password defaults group to Administrator and status to Enable, then always includes
both values in the account update. Resetting a disabled or non-administrator account without
overriding those optional arguments therefore also enables and elevates that account.
Code

anjoy/comm.py[R559-560]

+    def set_password(self, password: str, *, username: str = "admin",
+                     group: str = "Administrator", status: str = "Enable",
Evidence
The new method declares administrator/enabled defaults and unconditionally writes them, while the
account fixture proves devices can contain users with Group="User" and Status="Disable"; section
writes apply supplied fields through the device's merge behavior.

anjoy/comm.py[559-577]
anjoy/comm.py[381-400]
tests/test_comm.py[615-618]
docs/devices.md[167-177]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`set_password` always submits administrator and enabled defaults, so changing another account's password can also elevate or re-enable it.
## Fix Focus Areas
- anjoy/comm.py[559-578]
- tests/test_comm.py[614-645]
## Recommended Fix
Make `group` and `status` optional overrides. When either is omitted, preserve that field from the matching account returned by `get_users`, and add tests showing that resetting a disabled non-administrator account does not alter its group or status.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Reconnects use the previous password ✓ Resolved 🐞 Bug ≡ Correctness
Description
set_password returns after the acknowledged write without updating self.password when username
is the account represented by the client. After that client reconnects or is reused as a context
manager, login sends the previous password and authentication is rejected.
Code

anjoy/comm.py[578]

+        return self.set_config_section(const.CFG_USER, body, confirm=True)
Evidence
The constructor retains the credential in self.password, every later login reads that field, and
reconnection clears only session state; the new setter never synchronizes the field after changing
the device credential.

anjoy/comm.py[112-116]
anjoy/comm.py[127-136]
anjoy/comm.py[195-201]
anjoy/comm.py[559-578]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Changing the current client's account password leaves its stored login credential stale, causing subsequent authentication attempts to use the old password.
## Fix Focus Areas
- anjoy/comm.py[559-578]
- tests/test_comm.py[629-655]
## Recommended Fix
After `set_config_section` succeeds, update `self.password` when `username == self.user`, while leaving it unchanged for updates to other accounts. Add a test that changes the current account's password and verifies the next login frame contains the new credential.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can route each action level your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread anjoy/comm.py Outdated
Comment thread anjoy/comm.py Outdated
Two review findings:
1. Security — the old signature defaulted Group="Administrator", so resetting a
   non-admin user's password would elevate them to admin. set_password now
   read-modify-writes the target account: it preserves the account's existing
   Group/Status and only replaces the password (drops the stale EncryptPwd, adds
   plaintext). Raises if the username does not exist.
2. Correctness — after changing the password of the account the client logs in
   as, self.password was stale, so a reconnect failed. Now self.password is
   synced when username == self.user.

Re-verified live on MTF45-4G_AF (admin change + reconnect + revert). Tests:
group preserved, guest not escalated, self.password synced, unknown-user raises,
confirm gate, XML-escaping.
@widgetii

Copy link
Copy Markdown
Member Author

Addressed both findings: set_password now read-modify-writes the target account — it preserves the account's existing Group/Status (a guest reset no longer elevates to admin) and only replaces the password (drops the stale EncryptPwd, sends plaintext); it raises for an unknown username. And it syncs self.password when the changed account is the one the client authenticates as, so reconnects use the new password. Re-verified live (admin change + reconnect + revert); added tests for group preservation, the guest non-escalation, credential sync, and the unknown-user error.

@widgetii
widgetii merged commit d689f1d into master Sep 22, 2026
6 checks passed
@widgetii
widgetii deleted the setters-users branch September 22, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant