Repository navigation
Added successor user, user role, and SAML-managed group options to users and groups - #1041
Merged
Merged
Conversation
The users-delete API documents the path parameter as the user UUID, and the API accepts either the UUID or the integer ID. users.delete() only accepted an integer. It now also accepts a UUID string, normalized to lowercase and validated before the request is sent.
users.delete() now accepts a keyword-only successor_user_uuid, sent as the users-delete query parameter of the same name, so a departing user's objects can be transferred to another user. The UUID is normalized to lowercase and validated first, because the API answers a malformed UUID with 404 Not Found. The docstring records behavior observed against the live API: a successor without enough permissions returns 409 and the user is not deleted.
users.list() now accepts a keyword-only with_roles boolean, sent as the users-list withRoles query parameter in lowercase. When true, each user record includes an rbac_roles list with the name and UUID of each assigned role. The API silently ignores values other than true and false, so other types raise TypeError before the request is sent.
…5173) groups.create() and groups.edit() now accept a keyword-only managed_by_saml boolean, sent in the groups-create and groups-edit request bodies, to control whether SAML single sign-on manages group membership. When omitted, the request body is unchanged. Live testing showed that an edit without managed_by_saml keeps the current setting, and that the API requires the name on every edit; the docstrings say so.
SteveMcGrath
approved these changes
Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This adds three documented access-control options to
tio.usersandtio.groups. It also letsusers.delete()take the user UUID, which is what the API reference documents for its path. Every new argument is keyword-only and defaults toNone. When an argument is omitted, the request is exactly the same as before.There's one commit per change, so each can be reviewed or reverted independently.
users.delete(user_id)now accepts the integer ID or the UUIDusers.delete(..., successor_user_uuid=)successor_user_uuidquery parameterusers.list(with_roles=)withRolesquery parameter, as lowercasetrue/falsegroups.create(..., managed_by_saml=),groups.edit(..., managed_by_saml=)managed_by_samlin the request bodyValidation follows the modules' existing
_check()style:user_idandsuccessor_user_uuidare lowercased, then validated before any request is sent. The API answers a malformed or unknown successor UUID with404 Not Found, which reads like a missing user.with_rolesandmanaged_by_samlmust be booleans. The API silently ignoreswithRolesvalues other thantrueandfalse.The docstrings describe what the API actually does, based on live testing:
409and leaves the user in place.groups.edit()withoutmanaged_by_samlkeeps the current setting.groups.edit()still requiresname. The docstring says to pass the current name when changing only the flag.No new dependencies.
CHANGELOG.mdandtenable/version.pyare untouched.Fixes #1040
Internal tracking: RDC-25173.
Type of change
How Has This Been Tested?
Unit tests. They use
responsesand add no cassettes. They assert the exact path, query string (query_param_matcher) or body (json_params_matcherwithstrict_match=True).users.deleteIDs: accepts an integer, a numeric string, and lowercase or uppercase UUIDs, and sends the expected path. A non-UUID string or a float raises before any request.successor_user_uuid: sent lowercase. A malformed value raisesUnexpectedValueError, and a non-string raisesTypeError, both with no request sent. It can't be passed positionally.with_roles: omitted,TrueandFalseproduce no parameter,withRoles=trueandwithRoles=false. Invalid values raiseTypeErrorwith no request sent.managed_by_saml: omitted,TrueandFalseproduce the expectedcreateandeditbodies. Invalid values raiseTypeErrorwith no request sent. It can't be passed positionally.main: every test that sends a new value fails againstmain. Three groups pass there:mainalready supported;TypeErroronmainonly because it rejects the unknown keyword.ruff check tenableis clean, andruff formatreports no changes in the edited modules. The full suite passes on 3.11, 3.12, 3.13 and 3.14: 2201 passed, 0 failed, 95.6–95.8% coverage, and no new cassettes.bandit -llresults are unchanged frommain(no medium or high issues).main(the same 6 warnings), and the new arguments and examples render on the users and groups pages.Live API tests. These used the patched methods against three Tenable Vulnerability Management test environments. Throwaway users, scans and groups were created for the test and deleted afterward, with nothing left over. All 21 checks passed in each environment:
users.list(with_roles=True)addedrbac_roles(name,uuid) to every user (19, 29 and 87 users).users.list()andwith_roles=Falsereturned the same users without it.users.delete(A_uuid, successor_user_uuid=B_uuid). Both UUIDs were passed in uppercase.users.delete('<numeric id>')deleted the user.409APIError, and the user still existed. Invalid successors raised locally, and the user still existed.groups.create():managed_by_saml=TrueandFalsecame back in the create response and the group list. Omitting it left the property absent.groups.edit(): an edit withoutmanaged_by_samlkeptTrue.FalseandTruechanged it as expected.Test Configuration:
Checklist:
Notes for review
successor_user_uuid. In all three environments, the deleted user's scan went to the user making the request. Every requester was an administrator, though, so the API's actual rule is unconfirmed, and the docstring doesn't describe it.users.delete(None)still sendsDELETE users/None, as it did before this change, because_check()returnsNoneforNoneinput. I left that unchanged to keep this PR to the new options.