Skip to content

XML connector: update, delete and authenticate break on an entry below another element #182

Open

Description

@vharseko

Found while reviewing #181. Line references are to master at ccb33e9. Reproduced with XMLHandlerImpl directly and the test schema ef2bc95b-76e0-48e2-86d6-4d4f44d4e4a4.xsd, on master and on #181's head (6beb334): the results are the same on both.

getEntry finds an entry with the XQuery /icf:OpenICFContainer//ri:CLASS[...] (QueryBuilder.java#L97-L100), so it also finds an entry below another element, for example inside <ri:wrapper> or inside the value element of another entry. #181 keeps that: its EntryIndex answers with the same entries as the XQuery, nested ones included. The code that then works on the entry assumes a direct child of the container with no entries inside it:

  • update removes descendants as if they were children. removeChildrenFromElement collects the elements to replace with getElementsByTagName (L724-L735), which returns every descendant with that name, and calls element.removeChild on each. When the entry holds another entry, the first removeChild on the inner entry's element throws DOMException NOT_FOUND_ERR. The entry's own element has already been removed by then, and the document is already marked dirty (L333-L334), so dispose() saves the entry without it.
  • delete removes the entry from the container only. It calls getDocument().getDocumentElement().removeChild(entry) (L372), which throws DOMException NOT_FOUND_ERR for an entry whose parent is another element. The caller gets an unchecked DOMException, and the entry stays.
  • authenticate reads the password from any descendant. It takes entry.getElementsByTagName("icf:__PASSWORD__").item(0) (L463-L465). For an entry without its own __PASSWORD__ that holds an entry with one, the inner entry's password is accepted and the outer entry's Uid is returned. For an entry without any __PASSWORD__ element, item(0) is null and the call fails with a NullPointerException instead of InvalidPasswordException.

Observed, on a store with these entries:

<ri:__ACCOUNT__><icf:__UID__>u1</icf:__UID__><icf:__NAME__>nopass</icf:__NAME__><ri:lastname>L1</ri:lastname></ri:__ACCOUNT__>
<ri:wrapper><ri:__ACCOUNT__><icf:__UID__>u8</icf:__UID__><icf:__NAME__>nested</icf:__NAME__><icf:__PASSWORD__>p8</icf:__PASSWORD__><ri:lastname>L8</ri:lastname></ri:__ACCOUNT__></ri:wrapper>
<ri:__ACCOUNT__><icf:__UID__>u11</icf:__UID__><icf:__NAME__>holder</icf:__NAME__><ri:lastname><ri:__ACCOUNT__><icf:__UID__>u12</icf:__UID__><icf:__NAME__>inner</icf:__NAME__><icf:__PASSWORD__>p12</icf:__PASSWORD__></ri:__ACCOUNT__></ri:lastname></ri:__ACCOUNT__>
Call Result File after dispose()
update(ACCOUNT, Uid("u11"), {__NAME__ = "new"}) DOMException: NOT_FOUND_ERR u11 saved without __NAME__; u12 unchanged
delete(ACCOUNT, Uid("u8")) DOMException: NOT_FOUND_ERR unchanged, u8 still there
authenticate("holder", "p12") returns Uid u11 unchanged
authenticate("nopass", "x") NullPointerException (NodeList.item(int) is null) unchanged

Such a store does not validate against the schema (lastname is xsd:string, ri:wrapper is not declared, __PASSWORD__ is required), and the connector does not write this shape itself. But it does not validate the file on load either, and it treats the nested entries as entries everywhere else: search returns them, and #181 indexes them. So a hand-edited or generated file gets a half-applied update saved to disk, a delete that fails with an unchecked exception, and a login checked against another entry's password.

Possible fixes, one of them or both:

  • make the three operations work on the entry's own children: remove only the entry's child elements in update, remove the entry through entry.getParentNode() in delete, read __PASSWORD__ (and the __UID__ / __NAME__ that authenticate returns) from the entry's child elements only, and reject a missing password with InvalidPasswordException;
  • or stop treating nested elements as entries (/icf:OpenICFContainer/ri:CLASS instead of //, in QueryBuilder and in [#157] Find XML entries by __UID__ and __NAME__ through an index #181's EntryIndex), which changes what a search returns for such files.

Tests that depend on the current behaviour:

Activity

  1. added
    bugSomething isn't working
    data-lossFixes or risks loss or corruption of stored data
    on Oct 12, 2026
  2. self-assigned this
    on Oct 12, 2026
  3. maximthomas commented on Oct 12, 2026

    @maximthomas
    Contributor

    Two more cases in the same code, on a store that validates against the test schema. update and authenticate find the entry's elements by qualified name, so they depend on the prefixes the file uses and on whether an element is qualified at all. Neither case is about nesting, so they may fit #182's fix or an issue of their own.

    removeChildrenFromElement calls getElementsByTagName(childName) (L725) with the name that prefixAttributeName builds, icf: or ri: plus the attribute name (L334, L737-L747). authenticate reads icf:__PASSWORD__, icf:__UID__ and icf:__NAME__ the same way (L463, L477, L482). getElementsByTagName compares the tag name as written in the file, prefix included. getEntry finds the entry by namespace URI: on master its XQuery declares icf and ri by URI (QueryBuilder.java#L85-L95) over a namespace-aware DOM, and on #181 EntryIndex compares getNamespaceURI() and getLocalName(). ConnectorObjectCreator, which builds the search results, reads the entry's children by local name only (ConnectorObjectCreator.java#L79-L85). So getEntry and search see the entry's elements, and update's removal and authenticate's reads do not.

    Reproduced with XMLHandlerImpl through ConcurrentXMLHandler, and the searches also through XMLConnector, with the test schema ef2bc95b-76e0-48e2-86d6-4d4f44d4e4a4.xsd, on master (ccb33e9) and on #181's head (6beb334): the results are the same on both. The store, which xmllint --schema ef2bc95b-76e0-48e2-86d6-4d4f44d4e4a4.xsd validates:

    <x:OpenICFContainer xmlns:x="http://openidm.forgerock.com/xml/ns/public/resource/openicf/resource-schema-1.xsd"
                        xmlns:ri="http://openidm.forgerock.com/xml/ns/public/resource/instances/ef2bc95b-76e0-48e2-86d6-4d4f44d4e4a4">
    <ri:__ACCOUNT__><x:__UID__>u20</x:__UID__><x:__NAME__>pfx</x:__NAME__><lastname>L20</lastname><x:__PASSWORD__>p20</x:__PASSWORD__></ri:__ACCOUNT__>
    </x:OpenICFContainer>

    ICF elements under another prefix.

    Call Result File after dispose()
    authenticate("pfx", "p20") NullPointerException (NodeList.item(int) is null) unchanged
    authenticate("pfx", "wrong") NullPointerException unchanged
    update(ACCOUNT, Uid("u20"), {__NAME__ = "pfx2"}) returns Uid u20 the entry has two names: <x:__NAME__>pfx</x:__NAME__> stays and <icf:__NAME__ xmlns:icf="…/resource-schema-1.xsd">pfx2</icf:__NAME__> is appended

    After that update, a search for the old name pfx still returns u20, reported with the name pfx2, before and after a reload of the file. With the same store written with the icf prefix, the first authenticate returns u20, the second returns null (which XMLConnector.authenticate turns into InvalidPasswordException), and the update replaces the name.

    RI attributes. The test schema has no elementFormDefault, so its local elements, the RI attributes such as lastname, are in no namespace in a valid store, as in the one above. update(ACCOUNT, Uid("u20"), {lastname = "NEW"}) on that store, with the x prefix and with icf alike, keeps <lastname>L20</lastname> and appends <ri:lastname>NEW</ri:lastname>, and a search then reports lastname = NEW. With the RI namespace bound to another prefix (<r:lastname>, which the schema does not accept), the update also keeps the old element and appends <ri:lastname xmlns:ri="…">NEW</ri:lastname>.

    For the first of #182's possible fixes (working on the entry's own children), this means that finding a child by namespace URI and local name covers the ICF elements but not the unqualified RI attributes. Matching the way ConnectorObjectCreator reads, by local name among the entry's own children, would cover both, and update and search would agree on which elements an attribute has. getElementsByTagNameNS on the entry would fix the prefix but still descend into nested entries.

    A note on validity: the connector writes RI attributes qualified, as ri:lastname (L630-L631), which is also what writeAccounts writes for the tests, and xmllint rejects that (Element '{…/ef2bc95b-76e0-48e2-86d6-4d4f44d4e4a4}lastname': This element is not expected. Expected is one of ( firstname, lastname ).). The schema's container also takes a single element (<xsd:sequence><xsd:any/></xsd:sequence>, resource-schema-1.xsd#L45-L54), which is why the store above has one entry: a store with more entries fails for that reason too. So the stores the connector writes do not validate against this schema either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingconnector:xmlXML connectordata-lossFixes or risks loss or corruption of stored data

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions