Skip to content

[bug] Resending an unchanged is_admin value deletes the OrganizationUser membership #552

Description

@pandafy

Describe the bug

SuperUserDetailSerializer.update() in openwisp_users/api/serializers.py uses a non-obvious rule for organization_users: if the payload includes an existing membership and the submitted is_admin value is the same as the current value, the membership is deleted instead of being left unchanged.

This behavior is implemented here:

if org_user.is_admin != org_user_data["is_admin"]:
    org_user.is_admin = org_user_data["is_admin"]
    org_user.full_clean()
    org_user.save()
else:
    org_user.delete()

As a result, API clients that resend the current representation of a user on PATCH can unintentionally delete organization memberships. The only way to preserve an existing membership is to omit it entirely from the payload, or omit is_admin from that specific entry.

Why this is a problem

  • The delete-vs-update behavior is not documented in the REST API docs.
  • The serializer response representation includes organization_users, so a naive PATCH round-trip can be destructive.
  • This is easy to miss for API consumers who expect unchanged fields to remain unchanged.

Relevant code

  • openwisp_users/api/serializers.py
    • BaseSuperUserSerializer.to_representation() serializes organization_users with organization and is_admin.
    • SuperUserDetailSerializer.update() deletes the membership when is_admin matches the current value.
  • docs/user/rest-api.rst currently notes that is_admin represents the organization manager flag, but does not document the delete-on-unchanged behavior.
  • Existing tests such as test_remove_org_user_api and test_make_user_org_admin_api cover parts of this flow, but do not make the contract explicit in the docs.

Proposed resolution

Please make a product decision on one of these options:

  1. Document the current behavior explicitly, or
  2. Replace it with an explicit removal signal instead of overloading "unchanged" to mean "delete".

Option 2 would be a breaking API change for existing consumers, so it likely needs a migration plan or versioning strategy.

Notes

This is unrelated to the disabled-organization write protection work. The behavior predates that effort and appears to come from the original REST API implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions