/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2012-2015 ForgeRock AS */ package org.opends.server.extensions; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ConditionResult; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchScope; import org.mockito.ArgumentCaptor; import org.opends.server.TestCaseUtils; import org.opends.server.admin.server.ConfigurationChangeListener; import org.opends.server.admin.std.meta.EntityTagVirtualAttributeCfgDefn.ChecksumAlgorithm; import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn; import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn.ConflictBehavior; import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn.Scope; import org.opends.server.admin.std.server.EntityTagVirtualAttributeCfg; import org.opends.server.admin.std.server.VirtualAttributeCfg; import org.opends.server.controls.LDAPAssertionRequestControl; import org.opends.server.controls.LDAPPostReadRequestControl; import org.opends.server.controls.LDAPPostReadResponseControl; import org.opends.server.controls.LDAPPreReadRequestControl; import org.opends.server.controls.LDAPPreReadResponseControl; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyOperation; import org.opends.server.core.SearchOperation; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.protocols.internal.InternalSearchOperation; import org.opends.server.protocols.internal.Requests; import org.opends.server.protocols.internal.SearchRequest; import org.opends.server.protocols.ldap.LDAPFilter; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.Attributes; import org.opends.server.types.Control; import org.opends.server.types.DN; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.Modification; import org.opends.server.types.SearchFilter; import org.opends.server.types.VirtualAttributeRule; import org.opends.server.util.StaticUtils; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import static java.util.Collections.*; import static org.mockito.Mockito.*; import static org.opends.server.protocols.internal.InternalClientConnection.*; import static org.opends.server.util.CollectionUtils.*; import static org.testng.Assert.*; /** * A set of test cases for the entity tag virtual attribute provider. */ public class EntityTagVirtualAttributeProviderTestCase extends ExtensionsTestCase { private static final String DESCRIPTION = "description"; private static final String ETAG = "etag"; private final ByteString dummyValue = ByteString.valueOfUtf8("dummy"); private final EntityTagVirtualAttributeProvider provider = new EntityTagVirtualAttributeProvider(); private boolean changeListenerRemoved; private boolean changeListenerAdded; private final EntityTagVirtualAttributeCfg config = new EntityTagVirtualAttributeCfg() { private final TreeSet<AttributeType> excludedAttributes = new TreeSet<>(); @Override public void addChangeListener( final ConfigurationChangeListener<VirtualAttributeCfg> listener) { // Should not be called. throw new IllegalStateException(); } @Override public void addEntityTagChangeListener( final ConfigurationChangeListener<EntityTagVirtualAttributeCfg> listener) { changeListenerAdded = true; } @Override public Class<? extends EntityTagVirtualAttributeCfg> configurationClass() { // Not needed. return null; } @Override public DN dn() { // Not needed. return null; } @Override public AttributeType getAttributeType() { // Not needed. return null; } @Override public SortedSet<DN> getBaseDN() { // Not needed. return null; } @Override public ChecksumAlgorithm getChecksumAlgorithm() { return ChecksumAlgorithm.ADLER_32; } @Override public ConflictBehavior getConflictBehavior() { // Not needed. return null; } @Override public SortedSet<AttributeType> getExcludedAttribute() { return excludedAttributes; } @Override public SortedSet<String> getFilter() { // Not needed. return null; } @Override public SortedSet<DN> getGroupDN() { // Not needed. return null; } @Override public String getJavaClass() { // Not needed. return null; } @Override public Scope getScope() { // Not needed. return null; } @Override public boolean isEnabled() { return true; } @Override public void removeChangeListener( final ConfigurationChangeListener<VirtualAttributeCfg> listener) { // Should not be called. throw new IllegalStateException(); } @Override public void removeEntityTagChangeListener( final ConfigurationChangeListener<EntityTagVirtualAttributeCfg> listener) { changeListenerRemoved = true; } }; /** * Ensures that the Directory Server is running. * * @throws Exception * If an unexpected problem occurs. */ @BeforeClass public void startServer() throws Exception { TestCaseUtils.startServer(); // Initialize the provider. config.getExcludedAttribute().add(DirectoryServer.getAttributeTypeOrNull("modifytimestamp")); provider.initializeVirtualAttributeProvider(config); } /** * Tests that approximate matching is not supported. */ @Test public void testApproximatelyEqualTo() { assertEquals(provider.approximatelyEqualTo(null, null, null), ConditionResult.UNDEFINED); } /** * Tests that finalization removes the change listener. */ @Test public void testFinalizeVirtualAttributeProvider() { provider.finalizeVirtualAttributeProvider(); assertTrue(changeListenerRemoved); } /** * Tests the getValues method returns an ETag whose value represents a 64-bit * non-zero long encoded as hex. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testGetValuesBasic() throws Exception { final Entry e = TestCaseUtils.makeEntry("dn: dc=example,dc=com", "objectClass: top", "objectClass: domain", "dc: example"); getEntityTag(e, getRule()); } /** * Tests the getValues method returns a different value for entries which are * different. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testGetValuesDifferent() throws Exception { final Entry e1 = TestCaseUtils.makeEntry("dn: dc=example1,dc=com", "objectClass: top", "objectClass: domain", "dc: example1"); final Entry e2 = TestCaseUtils.makeEntry("dn: dc=example2,dc=com", "objectClass: top", "objectClass: domain", "dc: example2"); VirtualAttributeRule rule = getRule(); assertFalse(getEntityTag(e1, rule).equals(getEntityTag(e2, rule))); } /** * Tests the getValues method ignores excluded attributes. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testGetValuesIgnoresExcludedAttributes() throws Exception { final Entry e1 = TestCaseUtils.makeEntry("dn: dc=example,dc=com", "objectClass: top", "objectClass: domain", "dc: example"); final Entry e2 = TestCaseUtils.makeEntry("dn: dc=example,dc=com", "objectClass: top", "objectClass: domain", "dc: example", "modifyTimestamp: 20120222232918Z"); VirtualAttributeRule rule = getRule(); assertEquals(getEntityTag(e1, rule), getEntityTag(e2, rule)); } /** * Tests the getValues method returns the same value for entries having the * same content but with attributes in a different order. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testGetValuesNormalizedOrder() throws Exception { final Entry e1 = TestCaseUtils.makeEntry("dn: dc=example,dc=com", "objectClass: top", "objectClass: domain", "description: one", "description: two", "dc: example"); final Entry e2 = TestCaseUtils.makeEntry("dn: dc=example,dc=com", "objectClass: top", "objectClass: domain", "dc: example", "description: two", "description: one"); VirtualAttributeRule rule = getRule(); assertEquals(getEntityTag(e1, rule), getEntityTag(e2, rule)); } /** * Tests the getValues method returns the same value for different instances * of the same entry. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testGetValuesRepeatable() throws Exception { final Entry e1 = TestCaseUtils.makeEntry("dn: dc=example,dc=com", "objectClass: top", "objectClass: domain", "dc: example"); final Entry e2 = TestCaseUtils.makeEntry("dn: dc=example,dc=com", "objectClass: top", "objectClass: domain", "dc: example"); VirtualAttributeRule rule = getRule(); assertEquals(getEntityTag(e1, rule), getEntityTag(e2, rule)); } /** * Tests that ordering matching is not supported. */ @Test public void testGreaterThanOrEqualTo() { assertEquals(provider.greaterThanOrEqualTo(null, null, null), ConditionResult.UNDEFINED); } /** * Tests hasAllValues() membership. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testHasAllValues() throws Exception { final Entry e = TestCaseUtils.makeEntry("dn: dc=example,dc=com", "objectClass: top", "objectClass: domain", "dc: example"); VirtualAttributeRule rule = getRule(); final ByteString value = getEntityTag(e, rule); assertTrue(provider.hasAllValues(e, rule, Collections.<ByteString> emptySet())); assertTrue(provider.hasAllValues(e, rule, Collections.singleton(value))); assertFalse(provider.hasAllValues(e, rule, Collections.singleton(dummyValue))); assertFalse(provider.hasAllValues(e, rule, Arrays.asList(value, dummyValue))); } /** * Tests that the etags are always present. */ @Test public void testHasValue1() { assertTrue(provider.hasValue(null, null)); } /** * Tests testHasValue membership. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testHasValue2() throws Exception { final Entry e = TestCaseUtils.makeEntry("dn: dc=example,dc=com", "objectClass: top", "objectClass: domain", "dc: example"); VirtualAttributeRule rule = getRule(); final ByteString value = getEntityTag(e, rule); assertTrue(provider.hasValue(e, rule, value)); assertFalse(provider.hasValue(e, rule, dummyValue)); } /** * Tests that initialization adds the change listener. */ @Test public void testInitializeVirtualAttributeProvider() { // This was actually done during initialization of this test. Check that the // listener was registered. assertTrue(changeListenerAdded); } /** * Tests that isConfigurationAcceptable always returns true. */ @Test public void testIsConfigurationAcceptable() { assertTrue(provider.isConfigurationAcceptable(config, null)); } /** * Tests that the etags are single-valued. */ @Test public void testIsMultiValued() { assertFalse(provider.isMultiValued()); } /** * Tests that searching based on etag filters is not supported. */ @Test public void testIsSearchable() { assertFalse(provider.isSearchable(null, null, false)); assertFalse(provider.isSearchable(null, null, true)); } /** * Tests that ordering matching is not supported. */ @Test public void testLessThanOrEqualTo() { assertEquals(provider.lessThanOrEqualTo(null, null, null), ConditionResult.UNDEFINED); } /** * Tests that substring matching is not supported. */ @Test public void testMatchesSubstring() { assertEquals(provider.matchesSubstring(null, null, null, null, null), ConditionResult.UNDEFINED); } /** * Tests that searching based on etag filters is not supported. */ @Test public void testProcessSearch() { final SearchOperation searchOp = mock(SearchOperation.class); VirtualAttributeRule rule = new VirtualAttributeRule( DirectoryServer.getAttributeTypeOrNull(ETAG), provider, Collections.<DN> emptySet(), SearchScope.WHOLE_SUBTREE, Collections.<DN> emptySet(), Collections.<SearchFilter> emptySet(), VirtualAttributeCfgDefn.ConflictBehavior.REAL_OVERRIDES_VIRTUAL); provider.processSearch(rule, searchOp); final ArgumentCaptor<ResultCode> resultCode = ArgumentCaptor.forClass(ResultCode.class); verify(searchOp).setResultCode(resultCode.capture()); assertEquals(resultCode.getValue(), ResultCode.UNWILLING_TO_PERFORM); final ArgumentCaptor<LocalizableMessage> errorMsg = ArgumentCaptor.forClass(LocalizableMessage.class); verify(searchOp).appendErrorMessage(errorMsg.capture()); assertNotNull(errorMsg.getValue()); } /** * Simulates the main use case for entity tag support: optimistic concurrency. * <p> * This test reads an entry requesting its etag, then performs an update using * an assertion control to prevent the change from being applied if the etag * has changed since the read was performed. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testOptimisticConcurrency() throws Exception { // Use an internal connection. DN userDN = DN.valueOf("uid=test.user,ou=People,o=test"); InternalClientConnection conn = getRootConnection(); // Create a test backend containing the user entry to be modified. TestCaseUtils.initializeTestBackend(true); // @formatter:off TestCaseUtils.addEntries( "dn: ou=People,o=test", "objectClass: top", "objectClass: organizationalUnit", "ou: People", "", "dn: uid=test.user,ou=People,o=test", "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "uid: test.user", "givenName: Test", "sn: User", "cn: Test User", "userPassword: password"); // @formatter:on // Read the user entry and get the etag. Entry e1 = readEntry(userDN); String etag1 = e1.parseAttribute(ETAG).asString(); assertNotNull(etag1); // Apply a change using the assertion control for optimistic concurrency. Attribute attr = Attributes.create(DESCRIPTION, "first modify"); List<Modification> mods = newArrayList(new Modification(ModificationType.REPLACE, attr)); Control c = new LDAPAssertionRequestControl(true, LDAPFilter.createEqualityFilter(ETAG, ByteString.valueOfUtf8(etag1))); List<Control> ctrls = Collections.singletonList(c); ModifyOperation modifyOperation = conn.processModify(userDN, mods, ctrls); assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS); // Reread the entry and check that the description has been added and that // the etag has changed. Entry e2 = readEntry(userDN); String etag2 = e2.parseAttribute(ETAG).asString(); assertNotNull(etag2); assertFalse(etag1.equals(etag2)); String description2 = e2.parseAttribute(DESCRIPTION).asString(); assertNotNull(description2); assertEquals(description2, "first modify"); // Simulate a concurrent update: perform another update using the old etag. Attribute attr2 = Attributes.create(DESCRIPTION, "second modify"); mods = newArrayList(new Modification(ModificationType.REPLACE, attr2)); modifyOperation = conn.processModify(userDN, mods, ctrls); assertEquals(modifyOperation.getResultCode(), ResultCode.ASSERTION_FAILED); // Reread the entry and check that the description and etag have not changed Entry e3 = readEntry(userDN); String etag3 = e3.parseAttribute(ETAG).asString(); assertNotNull(etag3); assertEquals(etag2, etag3); String description3 = e3.parseAttribute(DESCRIPTION).asString(); assertNotNull(description3); assertEquals(description3, description2); } /** * Tests that the etag returned with a pre-read control after a modify * operation is correct. See OPENDJ-861. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testPreReadControl() throws Exception { DN userDN = DN.valueOf("uid=test.user,ou=People,o=test"); InternalClientConnection conn = getRootConnection(); // Create a test backend containing the user entry to be modified. TestCaseUtils.initializeTestBackend(true); // @formatter:off TestCaseUtils.addEntries( "dn: ou=People,o=test", "objectClass: top", "objectClass: organizationalUnit", "ou: People", "", "dn: uid=test.user,ou=People,o=test", "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "uid: test.user", "givenName: Test", "sn: User", "cn: Test User", "userPassword: password", "description: initial value"); // @formatter:on // Read the user entry and get the etag. Entry e1 = readEntry(userDN); String etag1 = e1.parseAttribute(ETAG).asString(); assertNotNull(etag1); // Apply a change using the pre and post read controls. Attribute attr = Attributes.create(DESCRIPTION, "modified value"); List<Modification> mods = newArrayList(new Modification(ModificationType.REPLACE, attr)); List<Control> ctrls = singletonList((Control) new LDAPPreReadRequestControl(true, singleton(ETAG))); ModifyOperation modifyOperation = conn.processModify(userDN, mods, ctrls); assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS); // Reread the entry and check that the description has been added and that // the etag has changed. Entry e2 = readEntry(userDN); String etag2 = e2.parseAttribute(ETAG).asString(); assertNotNull(etag2); assertFalse(etag1.equals(etag2)); String description2 = e2.parseAttribute(DESCRIPTION).asString(); assertNotNull(description2); assertEquals(description2, "modified value"); // Now check that the pre-read is the same as the initial etag. LDAPPreReadResponseControl preReadControl = getLDAPPreReadResponseControl(modifyOperation); String etagPreRead = preReadControl.getSearchEntry().parseAttribute(ETAG).asString(); assertEquals(etagPreRead, etag1); } /** * Tests that the etag returned with a post-read control after a modify * operation is correct. See OPENDJ-861. * * @throws Exception * If an unexpected exception occurred. */ @Test public void testPostReadControl() throws Exception { DN userDN = DN.valueOf("uid=test.user,ou=People,o=test"); InternalClientConnection conn = getRootConnection(); // Create a test backend containing the user entry to be modified. TestCaseUtils.initializeTestBackend(true); // @formatter:off TestCaseUtils.addEntries( "dn: ou=People,o=test", "objectClass: top", "objectClass: organizationalUnit", "ou: People", "", "dn: uid=test.user,ou=People,o=test", "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "uid: test.user", "givenName: Test", "sn: User", "cn: Test User", "userPassword: password", "description: initial value"); // @formatter:on // Read the user entry and get the etag. Entry e1 = readEntry(userDN); String etag1 = e1.parseAttribute(ETAG).asString(); assertNotNull(etag1); // Apply a change using the pre and post read controls. Attribute attr = Attributes.create(DESCRIPTION, "modified value"); List<Modification> mods = newArrayList(new Modification(ModificationType.REPLACE, attr)); List<Control> ctrls = singletonList((Control) new LDAPPostReadRequestControl(true, singleton(ETAG))); ModifyOperation modifyOperation = conn.processModify(userDN, mods, ctrls); assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS); // Reread the entry and check that the description has been added and that // the etag has changed. Entry e2 = readEntry(userDN); String etag2 = e2.parseAttribute(ETAG).asString(); assertNotNull(etag2); assertFalse(etag1.equals(etag2)); String description2 = e2.parseAttribute(DESCRIPTION).asString(); assertNotNull(description2); assertEquals(description2, "modified value"); // Now check that the post-read is the same as the initial etag. LDAPPostReadResponseControl postReadControl = getLDAPPostReadResponseControl(modifyOperation); String etagPostRead = postReadControl.getSearchEntry().parseAttribute(ETAG).asString(); assertEquals(etagPostRead, etag2); } private LDAPPostReadResponseControl getLDAPPostReadResponseControl(ModifyOperation modifyOperation) { for (Control control : modifyOperation.getResponseControls()) { if (control instanceof LDAPPostReadResponseControl) { return (LDAPPostReadResponseControl) control; } } fail("Expected the ModifyOperation to have a LDAPPostReadResponseControl"); return null; } private LDAPPreReadResponseControl getLDAPPreReadResponseControl(ModifyOperation modifyOperation) { for (Control control : modifyOperation.getResponseControls()) { if (control instanceof LDAPPreReadResponseControl) { return (LDAPPreReadResponseControl) control; } } fail("Expected the ModifyOperation to have a LDAPPreReadResponseControl"); return null; } private Entry readEntry(DN userDN) throws DirectoryException { SearchRequest request = Requests.newSearchRequest(userDN, SearchScope.BASE_OBJECT).addAttribute("*", ETAG); InternalSearchOperation searchOperation = getRootConnection().processSearch(request); assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS); assertEquals(searchOperation.getSearchEntries().size(), 1); Entry e = searchOperation.getSearchEntries().get(0); assertNotNull(e); return e; } private ByteString getEntityTag(final Entry e, VirtualAttributeRule rule) { final Attribute values = provider.getValues(e, rule); assertEquals(values.size(), 1); final ByteString value = values.iterator().next(); assertEquals(value.length(), 16); for (int i = 0; i < 16; i++) { assertTrue(StaticUtils.isHexDigit(value.byteAt(i))); if (value.byteAt(i) != 0x30) { return value; } } fail("Expected to find a non zero byte"); return null; } private VirtualAttributeRule getRule() { AttributeType type = DirectoryServer.getAttributeTypeOrNull("etag"); return new VirtualAttributeRule(type, provider, Collections.<DN>emptySet(), SearchScope.WHOLE_SUBTREE, Collections.<DN>emptySet(), Collections.<SearchFilter>emptySet(), VirtualAttributeCfgDefn.ConflictBehavior.VIRTUAL_OVERRIDES_REAL); } }