[#1158] Split a check-references-filter-criteria value at its first colon, so the filter can contain one (#1167)
Fixes #1158
## Problem
The referential integrity plugin split each
`check-references-filter-criteria` value (`attribute:filter`) at the
**last** colon, both when it validated the configuration and when it
applied it. An attribute description cannot contain `:`, but a filter
often does — a URN or URL value (`(o=urn:example)`), or an extensible
match (`(cn:dn:=x)`) — so such a value was refused: `member:(cn:dn` was
taken for the attribute and `=x)` for the filter.
There was a second, quieter difference between the two paths: only
validation trimmed the parts. `manager :(objectClass=person)` passed
validation, but the apply path looked up the attribute `"manager "`, got
a placeholder type, and the filter was never enforced.
## Fix
Both paths now call one helper, `splitFilterCriteria`, that splits at
the **first** colon and trims both parts. The property syntax
(`^[^:]+:\(.+\)$`) already guarantees a colon after a non-empty,
colon-free attribute, so the first colon is always the separator.
## Tests
`ReferentialIntegrityPluginTestCase`:
- `validConfigs` gains a configuration with `member:(o=urn:example)` and
`member:(cn:dn:=x)`.
- New `testEnforceIntegrityWithColonInFilter`, for
`manager:(description=urn:example:manager)`,
`manager:(description:caseExactMatch:=urn:example:manager)` and `manager
:(description=*manager)`: the configuration change is accepted, adding
an employee whose manager has no `description` is refused with
`CONSTRAINT_VIOLATION`, and it succeeds once the manager's `description`
is `urn:example:manager`.
Without the fix all four new cases fail: the colon cases because the
configuration is refused (`Unwilling to Perform`), and the
trailing-space case because the configuration is accepted but the filter
is not enforced (`expected [Constraint Violation] but found [Success]`).
With the fix the class passes 65/65.
## Follow-up
Two `check-references-filter-criteria` values for the same attribute
type are both accepted, but only one of them is enforced. This was
already true before this change and is tracked in #1172.
| | |
| | | |
| | | for (String attrFilt : newConfiguration.getCheckReferencesFilterCriteria()) |
| | | { |
| | | int sepInd = attrFilt.lastIndexOf(":"); |
| | | String attr = attrFilt.substring(0, sepInd); |
| | | String filtStr = attrFilt.substring(sepInd + 1); |
| | | String[] attrAndFilter = splitFilterCriteria(attrFilt); |
| | | String attr = attrAndFilter[0]; |
| | | String filtStr = attrAndFilter[1]; |
| | | |
| | | AttributeType attrType = DirectoryServer.getInstance().getServerContext().getSchema().getAttributeType(attr); |
| | | try |
| | |
| | | |
| | | for (String attrFilt : pluginCfg.getCheckReferencesFilterCriteria()) |
| | | { |
| | | int sepInd = attrFilt.lastIndexOf(":"); |
| | | String attr = attrFilt.substring(0, sepInd).trim(); |
| | | String filtStr = attrFilt.substring(sepInd + 1).trim(); |
| | | String[] attrAndFilter = splitFilterCriteria(attrFilt); |
| | | String attr = attrAndFilter[0]; |
| | | String filtStr = attrAndFilter[1]; |
| | | |
| | | /* TODO: strip the ;options part? */ |
| | | |
| | |
| | | return isAcceptable; |
| | | } |
| | | |
| | | /** |
| | | * Splits a check-references-filter-criteria value ({@code attribute:filter}) at its first colon: an attribute |
| | | * description cannot contain one, but the filter that follows it can, as in {@code (o=urn:example)} or |
| | | * {@code (cn:dn:=x)}. The property syntax guarantees that the value has a colon after a non-empty attribute. |
| | | * |
| | | * @param attrFilt |
| | | * The check-references-filter-criteria value. |
| | | * @return The attribute and the filter, both trimmed. |
| | | */ |
| | | private static String[] splitFilterCriteria(String attrFilt) |
| | | { |
| | | int sepInd = attrFilt.indexOf(':'); |
| | | return new String[] { attrFilt.substring(0, sepInd).trim(), attrFilt.substring(sepInd + 1).trim() }; |
| | | } |
| | | |
| | | @Override |
| | | public boolean isConfigurationChangeAcceptable( |
| | | ReferentialIntegrityPluginCfg configuration, |
| | |
| | | "ds-cfg-plugin-type: preOperationModify", |
| | | "ds-cfg-attribute-type: member", |
| | | "ds-cfg-check-references: false", |
| | | "ds-cfg-check-references-filter-criteria: member:(objectclass=person)" |
| | | "ds-cfg-check-references-filter-criteria: member:(objectclass=person)", |
| | | "", |
| | | // check-references enabled, filters that contain a colon |
| | | "dn: cn=Referential Integrity,cn=Plugins,cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-plugin", |
| | | "objectClass: ds-cfg-referential-integrity-plugin", |
| | | "cn: Referential Integrity", |
| | | "ds-cfg-java-class: org.opends.server.plugins.ReferentialIntegrityPlugin", |
| | | "ds-cfg-enabled: true", |
| | | "ds-cfg-plugin-type: postOperationDelete", |
| | | "ds-cfg-plugin-type: postOperationModifyDN", |
| | | "ds-cfg-plugin-type: subordinateModifyDN", |
| | | "ds-cfg-plugin-type: preOperationAdd", |
| | | "ds-cfg-plugin-type: preOperationModify", |
| | | "ds-cfg-attribute-type: member", |
| | | "ds-cfg-base-dn: o=test", |
| | | "ds-cfg-check-references: true", |
| | | "ds-cfg-check-references-filter-criteria: member:(o=urn:example)", |
| | | "ds-cfg-check-references-filter-criteria: member:(cn:dn:=x)" |
| | | ); |
| | | Object[][] array = new Object[entries.size()][1]; |
| | | for (int i=0; i < array.length; i++) |
| | |
| | | } |
| | | |
| | | /** |
| | | * Filter criteria whose filter contains a colon, or whose attribute is followed by a space. Each filter matches the |
| | | * manager entry only once its description is {@code urn:example:manager}. |
| | | */ |
| | | @DataProvider |
| | | public Object[][] filterCriteriaWithColonInFilter() |
| | | { |
| | | return new Object[][] { |
| | | { "manager:(description=urn:example:manager)" }, |
| | | { "manager:(description:caseExactMatch:=urn:example:manager)" }, |
| | | { "manager :(description=*manager)" }, |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * A check-references-filter-criteria value is split at its first colon, and both of its parts are trimmed: the |
| | | * filter that follows the attribute can contain colons of its own, and is enforced as written. |
| | | */ |
| | | @Test(dataProvider = "filterCriteriaWithColonInFilter") |
| | | public void testEnforceIntegrityWithColonInFilter(String filterCriteria) throws Exception |
| | | { |
| | | replaceAttrEntry(configDN, "ds-cfg-enabled", "false"); |
| | | replaceAttrEntry(configDN, dsConfigPluginType, |
| | | "postoperationdelete", |
| | | "postoperationmodifydn", |
| | | "subordinatemodifydn", |
| | | "subordinatedelete", |
| | | "preoperationadd", |
| | | "preoperationmodify"); |
| | | addAttrEntry(configDN, dsConfigBaseDN, "dc=example,dc=com"); |
| | | replaceAttrEntry(configDN, dsConfigEnforceIntegrity, "true"); |
| | | replaceAttrEntry(configDN, dsConfigAttrType, "manager"); |
| | | assertEquals(addAttrEntry(configDN, dsConfigAttrFiltMapping, filterCriteria).getResultCode(), ResultCode.SUCCESS); |
| | | assertEquals(replaceAttrEntry(configDN, "ds-cfg-enabled", "true").getResultCode(), ResultCode.SUCCESS); |
| | | |
| | | String manager = "uid=manager,ou=people,ou=dept,dc=example,dc=com"; |
| | | addEntry(manager); |
| | | Entry employee = TestCaseUtils.makeEntry( |
| | | "dn: uid=employee,ou=people,ou=dept,dc=example,dc=com", |
| | | "objectclass: top", |
| | | "objectclass: person", |
| | | "objectclass: organizationalperson", |
| | | "objectclass: inetorgperson", |
| | | "uid: employee", |
| | | "cn: employee", |
| | | "sn: employee", |
| | | "givenname: employee", |
| | | "manager: " + manager); |
| | | |
| | | assertEquals(getRootConnection().processAdd(employee).getResultCode(), ResultCode.CONSTRAINT_VIOLATION); |
| | | |
| | | assertEquals(addAttrEntry(DN.valueOf(manager), "description", "urn:example:manager").getResultCode(), |
| | | ResultCode.SUCCESS); |
| | | assertEquals(getRootConnection().processAdd(employee).getResultCode(), ResultCode.SUCCESS); |
| | | } |
| | | |
| | | /** |
| | | * Test case: |
| | | * - integrity is enforced on the attribute 'manager' |
| | | * - value of the 'manager' attribute should match the filter: |