This is an automated email from the ASF dual-hosted git repository. orudyy pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git
commit db639ff523b2cf28f8a64b2d06e54b5534182111 Author: Dedeepya T <dedeepy...@yahoo.co.in> AuthorDate: Tue Jun 8 10:53:18 2021 +0530 QPID-8525:[Broker-J]Fixed child deletion issue in group providers This closes #88 --- .../model/adapter/FileBasedGroupProviderImpl.java | 14 +- .../server/security/group/FileGroupDatabase.java | 57 ++++++-- .../adapter/FileBasedGroupProviderImplTest.java | 156 ++++++++++++++++++--- .../FileGroupDatabaseCaseInsensitiveTest.java | 29 +++- .../security/group/FileGroupDatabaseTest.java | 13 +- 5 files changed, 213 insertions(+), 56 deletions(-) diff --git a/broker-core/src/main/java/org/apache/qpid/server/model/adapter/FileBasedGroupProviderImpl.java b/broker-core/src/main/java/org/apache/qpid/server/model/adapter/FileBasedGroupProviderImpl.java index 7760f7b..8e108ec 100644 --- a/broker-core/src/main/java/org/apache/qpid/server/model/adapter/FileBasedGroupProviderImpl.java +++ b/broker-core/src/main/java/org/apache/qpid/server/model/adapter/FileBasedGroupProviderImpl.java @@ -203,19 +203,19 @@ public class FileBasedGroupProviderImpl if (getState() != State.ACTIVE) { - throw new IllegalConfigurationException(String.format("Group provider '%s' is not activated. Cannot create a group.", getName())); + throw new IllegalConfigurationException(String.format( + "Group provider '%s' is not activated. Cannot create a group.", + getName())); } - _groupDatabase.createGroup(groupName); - Map<String,Object> attrMap = new HashMap<String, Object>(); + Map<String, Object> attrMap = new HashMap<String, Object>(); UUID id = UUID.randomUUID(); attrMap.put(ConfiguredObject.ID, id); attrMap.put(ConfiguredObject.NAME, groupName); GroupAdapter groupAdapter = new GroupAdapter(attrMap); groupAdapter.create(); return Futures.immediateFuture((C) groupAdapter); - } else { @@ -359,16 +359,14 @@ public class FileBasedGroupProviderImpl if (childClass == GroupMember.class) { String memberName = (String) attributes.get(GroupMember.NAME); - _groupDatabase.addUserToGroup(memberName, getName()); UUID id = UUID.randomUUID(); - Map<String,Object> attrMap = new HashMap<String, Object>(); - attrMap.put(GroupMember.ID,id); + Map<String, Object> attrMap = new HashMap<String, Object>(); + attrMap.put(GroupMember.ID, id); attrMap.put(GroupMember.NAME, memberName); GroupMemberAdapter groupMemberAdapter = new GroupMemberAdapter(attrMap); groupMemberAdapter.create(); return Futures.immediateFuture((C) groupMemberAdapter); - } else { diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/group/FileGroupDatabase.java b/broker-core/src/main/java/org/apache/qpid/server/security/group/FileGroupDatabase.java index 8f08ccb..8d908e6 100644 --- a/broker-core/src/main/java/org/apache/qpid/server/security/group/FileGroupDatabase.java +++ b/broker-core/src/main/java/org/apache/qpid/server/security/group/FileGroupDatabase.java @@ -36,6 +36,7 @@ import com.google.common.base.Joiner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.qpid.server.configuration.IllegalConfigurationException; import org.apache.qpid.server.model.adapter.FileBasedGroupProvider; import org.apache.qpid.server.util.BaseAction; import org.apache.qpid.server.util.FileHelper; @@ -104,8 +105,10 @@ public class FileGroupDatabase implements GroupDatabase @Override public synchronized void addUserToGroup(String user, String group) { - Set<String> users = _groupToUserMap.get(keySearch(_groupToUserMap.keySet(), group)); - if (users == null) + final String groupKey = keySearch(_groupToUserMap.keySet(), group); + Set<String> groupUsers = _groupToUserMap.get(groupKey); + final String userKey = keySearch(_userToGroupMap.keySet(), user); + if (groupUsers == null) { throw new IllegalArgumentException("Group " + group @@ -113,16 +116,30 @@ public class FileGroupDatabase implements GroupDatabase + user + " to it"); } + else if (groupUsers.contains(userKey)) + { + throw new IllegalConfigurationException(String.format("Group member with name '%s' already exists", user)); + } - users.add(keySearch(users, user)); - - Set<String> groups = _userToGroupMap.get(keySearch(_userToGroupMap.keySet(), user)); + final String groupUserKey = keySearch(groupUsers, user); + if (!userKey.equals(groupUserKey)) + { + throw new IllegalConfigurationException(String.format( + "Inconsistent data: user key '%s' is not equal to a group key '%s'", + userKey, + groupKey)); + } + else + { + groupUsers.add(groupUserKey); + } + Set<String> groups = _userToGroupMap.get(userKey); if (groups == null) { groups = new ConcurrentSkipListSet<String>(); _userToGroupMap.put(user, groups); } - groups.add(keySearch(_groupToUserMap.keySet(), group)); + groups.add(groupKey); update(); } @@ -174,10 +191,32 @@ public class FileGroupDatabase implements GroupDatabase @Override public synchronized void createGroup(String group) { - Set<String> users = new ConcurrentSkipListSet<String>(); - _groupToUserMap.put(group, users); + if (!exists(group, _groupToUserMap.keySet())) + { + Set<String> users = new ConcurrentSkipListSet<String>(); + _groupToUserMap.put(group, users); + update(); + } + else + { + throw new IllegalConfigurationException(String.format("Group with name '%s' already exists", group)); + } + } - update(); + private boolean exists(final String searchString, final Set<String> set) + { + if (!_groupProvider.isCaseSensitive()) + { + for (String key : set) + { + if (key.equalsIgnoreCase(searchString)) + { + return true; + } + } + return false; + } + return set.contains(searchString); } @Override diff --git a/broker-core/src/test/java/org/apache/qpid/server/model/adapter/FileBasedGroupProviderImplTest.java b/broker-core/src/test/java/org/apache/qpid/server/model/adapter/FileBasedGroupProviderImplTest.java index 8a5c32e..410c913 100644 --- a/broker-core/src/test/java/org/apache/qpid/server/model/adapter/FileBasedGroupProviderImplTest.java +++ b/broker-core/src/test/java/org/apache/qpid/server/model/adapter/FileBasedGroupProviderImplTest.java @@ -29,6 +29,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.Mockito.when; @@ -45,6 +46,7 @@ import java.util.stream.Collectors; import com.google.common.collect.Sets; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -55,6 +57,7 @@ import org.apache.qpid.server.model.ConfiguredObjectFactory; import org.apache.qpid.server.model.Group; import org.apache.qpid.server.model.GroupMember; import org.apache.qpid.server.model.GroupProvider; +import org.apache.qpid.server.security.group.FileGroupDatabase; import org.apache.qpid.test.utils.TestFileUtils; import org.apache.qpid.test.utils.UnitTestBase; @@ -90,7 +93,7 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase @Test public void testValidationOnCreateWithInvalidPath() { - Map<String,Object> attributes = new HashMap<>(); + Map<String, Object> attributes = new HashMap<>(); _groupFile = TestFileUtils.createTempFile(this, "groups"); String groupsFile = _groupFile.getAbsolutePath() + File.separator + "groups"; @@ -103,12 +106,12 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase { _objectFactory.create(GroupProvider.class, attributes, _broker); fail("Exception is expected on validation of groups provider with invalid path"); - } catch (IllegalConfigurationException e) + } + catch (IllegalConfigurationException e) { assertEquals("Unexpected exception message:" + e.getMessage(), - String.format("Cannot create groups file at '%s'", groupsFile), - e.getMessage()); - + String.format("Cannot create groups file at '%s'", groupsFile), + e.getMessage()); } } @@ -130,8 +133,8 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase catch (IllegalConfigurationException e) { assertEquals("Unexpected exception message:" + e.getMessage(), - String.format("Cannot load groups from '%s'", groupsFile), - e.getMessage()); + String.format("Cannot load groups from '%s'", groupsFile), + e.getMessage()); } } @@ -186,15 +189,15 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase @SuppressWarnings("unchecked") GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); assertThat(provider, is(instanceOf(FileBasedGroupProvider.class))); - assertThat(((FileBasedGroupProvider)provider).isCaseSensitive(), is(true)); + assertThat(((FileBasedGroupProvider) provider).isCaseSensitive(), is(true)); Set<Principal> adminGroups = provider.getGroupPrincipalsForUser(() -> "Root"); assertThat("No group should be found when caseSensitive=true", adminGroups.stream().map(Principal::getName).collect(Collectors.toSet()), - is(empty())); + is(empty())); provider.setAttributes(Collections.singletonMap("caseSensitive", false)); - assertThat(((FileBasedGroupProvider)provider).isCaseSensitive(), is(false)); + assertThat(((FileBasedGroupProvider) provider).isCaseSensitive(), is(false)); Set<Principal> adminGroups2 = provider.getGroupPrincipalsForUser(() -> "Root"); assertThat("root has unexpected group membership", adminGroups2.stream().map(Principal::getName).collect(Collectors.toSet()), @@ -230,7 +233,7 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase public void testRemoveGroupAndMember() throws Exception { Map<String, Set<String>> input = new HashMap<>(); - input.put("supers", Sets.newHashSet( "root")); + input.put("supers", Sets.newHashSet("root")); input.put("operators", Sets.newHashSet("operator", "root")); _groupFile = createTemporaryGroupFile(input); @@ -270,8 +273,8 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase providerAttrs.put(FileBasedGroupProvider.NAME, getTestName()); { - @SuppressWarnings("unchecked") - final GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); + @SuppressWarnings("unchecked") final GroupProvider<?> provider = + _objectFactory.create(GroupProvider.class, providerAttrs, _broker); assertThat(provider.getChildren(Group.class).size(), is(equalTo(0))); final Map<String, Object> groupAttrs = Collections.singletonMap(Group.NAME, "group"); @@ -284,8 +287,8 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase } { - @SuppressWarnings("unchecked") - final GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); + @SuppressWarnings("unchecked") final GroupProvider<?> provider = + _objectFactory.create(GroupProvider.class, providerAttrs, _broker); assertThat(provider.getChildren(Group.class).size(), is(equalTo(1))); final Group group = provider.getChildByName(Group.class, "group"); @@ -297,8 +300,8 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase } { - @SuppressWarnings("unchecked") - final GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); + @SuppressWarnings("unchecked") final GroupProvider<?> provider = + _objectFactory.create(GroupProvider.class, providerAttrs, _broker); final Group group = provider.getChildByName(Group.class, "group"); assertThat(group.getChildren(GroupMember.class).size(), is(equalTo(0))); @@ -307,8 +310,8 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase } { - @SuppressWarnings("unchecked") - final GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); + @SuppressWarnings("unchecked") final GroupProvider<?> provider = + _objectFactory.create(GroupProvider.class, providerAttrs, _broker); assertThat(provider.getChildren(Group.class).size(), is(equalTo(0))); provider.close(); } @@ -325,8 +328,8 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase providerAttrs.put(FileBasedGroupProvider.PATH, groupsFile); providerAttrs.put(FileBasedGroupProvider.NAME, getTestName()); - @SuppressWarnings("unchecked") - final GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); + @SuppressWarnings("unchecked") final GroupProvider<?> provider = + _objectFactory.create(GroupProvider.class, providerAttrs, _broker); provider.delete(); assertThat(_groupFile.exists(), is(equalTo(false))); @@ -364,6 +367,115 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase } } + @Test + public void testCreateDuplicateGroup() throws Exception + { + _groupFile = createTemporaryGroupFile(Collections.emptyMap()); + + Map<String, Object> providerAttrs = new HashMap<>(); + String groupsFile = _groupFile.getAbsolutePath(); + providerAttrs.put(FileBasedGroupProvider.TYPE, GROUP_FILE_PROVIDER_TYPE); + providerAttrs.put(FileBasedGroupProvider.PATH, groupsFile); + providerAttrs.put(FileBasedGroupProvider.NAME, getTestName()); + + @SuppressWarnings("unchecked") + GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); + + assertThat(provider.getChildren(Group.class).size(), is(equalTo(0))); + + final Map<String, Object> groupAttrs = Collections.singletonMap(Group.NAME, "supers"); + Group superGroup = provider.createChild(Group.class, groupAttrs); + assertThat(superGroup.getName(), is(equalTo("supers"))); + + assertThrows("Group member with name root1 already exists", IllegalConfigurationException.class, ()->provider.createChild(Group.class, groupAttrs)); + } + + @Test + public void testCreateDuplicateMember() throws Exception + { + _groupFile = createTemporaryGroupFile(Collections.emptyMap()); + + Map<String, Object> providerAttrs = new HashMap<>(); + String groupsFile = _groupFile.getAbsolutePath(); + providerAttrs.put(FileBasedGroupProvider.TYPE, GROUP_FILE_PROVIDER_TYPE); + providerAttrs.put(FileBasedGroupProvider.PATH, groupsFile); + providerAttrs.put(FileBasedGroupProvider.NAME, getTestName()); + + @SuppressWarnings("unchecked") + GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); + + assertThat(provider.getChildren(Group.class).size(), is(equalTo(0))); + + final Map<String, Object> groupAttrs = Collections.singletonMap(Group.NAME, "supers"); + Group superGroup = provider.createChild(Group.class, groupAttrs); + assertThat(superGroup.getName(), is(equalTo("supers"))); + + final Map<String, Object> memberAttrs1 = Collections.singletonMap(GroupMember.NAME, "root1"); + GroupMember rootMember = (GroupMember) superGroup.createChild(GroupMember.class, memberAttrs1); + assertThat(rootMember.getName(), is(equalTo("root1"))); + + assertThrows("Group member with name root1 already exists", IllegalConfigurationException.class, ()->superGroup.createChild(GroupMember.class, memberAttrs1)); + assertThat(superGroup.getChildren(GroupMember.class).size(), is(equalTo(1))); + } + + @Test + public void testCreateGroups() throws Exception + { + _groupFile = createTemporaryGroupFile(Collections.emptyMap()); + + Map<String, Object> providerAttrs = new HashMap<>(); + String groupsFile = _groupFile.getAbsolutePath(); + providerAttrs.put(FileBasedGroupProvider.TYPE, GROUP_FILE_PROVIDER_TYPE); + providerAttrs.put(FileBasedGroupProvider.PATH, groupsFile); + providerAttrs.put(FileBasedGroupProvider.NAME, getTestName()); + + @SuppressWarnings("unchecked") + GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); + + assertThat(provider.getChildren(Group.class).size(), is(equalTo(0))); + + final Map<String, Object> groupAttrs = Collections.singletonMap(Group.NAME, "supers"); + Group superGroup = provider.createChild(Group.class, groupAttrs); + assertThat(superGroup.getName(), is(equalTo("supers"))); + + final Map<String, Object> groupAttrs2 = Collections.singletonMap(Group.NAME, "Supers"); + Group superGroup2 = provider.createChild(Group.class, groupAttrs2); + assertThat(superGroup2.getName(), is(equalTo("Supers"))); + + assertThat(provider.getChildren(Group.class).size(), is(equalTo(2))); + } + + @Test(expected = IllegalConfigurationException.class) + public void testCreateMembers() throws Exception + { + _groupFile = createTemporaryGroupFile(Collections.emptyMap()); + + Map<String, Object> providerAttrs = new HashMap<>(); + String groupsFile = _groupFile.getAbsolutePath(); + providerAttrs.put(FileBasedGroupProvider.TYPE, GROUP_FILE_PROVIDER_TYPE); + providerAttrs.put(FileBasedGroupProvider.PATH, groupsFile); + providerAttrs.put(FileBasedGroupProvider.NAME, getTestName()); + + @SuppressWarnings("unchecked") + GroupProvider<?> provider = _objectFactory.create(GroupProvider.class, providerAttrs, _broker); + + assertThat(provider.getChildren(Group.class).size(), is(equalTo(0))); + + final Map<String, Object> groupAttrs = Collections.singletonMap(Group.NAME, "supers"); + Group superGroup = provider.createChild(Group.class, groupAttrs); + assertThat(superGroup.getName(), is(equalTo("supers"))); + + final Map<String, Object> memberAttrs1 = Collections.singletonMap(GroupMember.NAME, "root1"); + GroupMember rootMember = (GroupMember) superGroup.createChild(GroupMember.class, memberAttrs1); + assertThat(rootMember.getName(), is(equalTo("root1"))); + + superGroup.createChild(GroupMember.class, memberAttrs1); + assertThrows("Group member with name root1 already exists", + IllegalConfigurationException.class, + () -> superGroup.createChild(GroupMember.class, memberAttrs1)); + assertThat(superGroup.getChildren(GroupMember.class).size(), is(equalTo(1))); + } + private File createTemporaryGroupFile(Map<String, Set<String>> groups) throws Exception { File groupFile = File.createTempFile("group", "grp"); @@ -378,7 +490,7 @@ public class FileBasedGroupProviderImplTest extends UnitTestBase .stream() .collect(Collectors.joining(",")))); props.putAll(m); - try(final FileOutputStream out = new FileOutputStream(groupFile)) + try (final FileOutputStream out = new FileOutputStream(groupFile)) { props.store(out, "test group file"); } diff --git a/broker-core/src/test/java/org/apache/qpid/server/security/group/FileGroupDatabaseCaseInsensitiveTest.java b/broker-core/src/test/java/org/apache/qpid/server/security/group/FileGroupDatabaseCaseInsensitiveTest.java index 2f0a923..2288977 100644 --- a/broker-core/src/test/java/org/apache/qpid/server/security/group/FileGroupDatabaseCaseInsensitiveTest.java +++ b/broker-core/src/test/java/org/apache/qpid/server/security/group/FileGroupDatabaseCaseInsensitiveTest.java @@ -21,6 +21,7 @@ package org.apache.qpid.server.security.group; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -34,6 +35,8 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.apache.qpid.server.configuration.IllegalConfigurationException; +import org.apache.qpid.server.model.Group; import org.apache.qpid.server.model.adapter.FileBasedGroupProvider; import org.apache.qpid.test.utils.UnitTestBase; @@ -168,20 +171,15 @@ public class FileGroupDatabaseCaseInsensitiveTest extends UnitTestBase assertTrue(groups.contains(MY_GROUP1)); } - @Test + @Test(expected = IllegalConfigurationException.class) public void testGetGroupPrincipalsForUserWhenUserAddedToGroupTheyAreAlreadyInCaseInsensitive() throws Exception { _util.writeAndSetGroupFile("myGroup.users", USER1); _fileGroupDatabase.addUserToGroup(USER1, MY_GROUP); Set<String> groups = _fileGroupDatabase.getGroupsForUser(USER1.toUpperCase()); + assertThrows("Group with name supers already exists", IllegalConfigurationException.class, ()->_fileGroupDatabase.addUserToGroup(USER1, MY_GROUP)); - assertEquals(1, groups.size()); - assertTrue(groups.contains(MY_GROUP)); - - Set<String> users = _fileGroupDatabase.getUsersInGroup(MY_GROUP.toUpperCase()); - assertEquals(1, users.size()); - assertTrue(users.contains(USER1)); } @Test @@ -361,6 +359,23 @@ public class FileGroupDatabaseCaseInsensitiveTest extends UnitTestBase } @Test + public void testDuplicateCreateGroupPersistedToFileCaseInsensitive() throws Exception + { + _util.writeAndSetGroupFile(); + + Set<String> groups = _fileGroupDatabase.getAllGroups(); + assertTrue(groups.isEmpty()); + + _fileGroupDatabase.createGroup(MY_GROUP); + + groups = _fileGroupDatabase.getAllGroups(); + assertEquals(1, groups.size()); + assertTrue(groups.contains(MY_GROUP)); + + assertThrows("Group with name myGroup already exists", IllegalConfigurationException.class, ()->_fileGroupDatabase.createGroup(MY_GROUP)); + } + + @Test public void testRemoveGroupPersistedToFileCaseInsensitive() throws Exception { _util.writeAndSetGroupFile("myGroup1.users", "user1,user2", "myGroup2.users", "user1,user2"); diff --git a/broker-core/src/test/java/org/apache/qpid/server/security/group/FileGroupDatabaseTest.java b/broker-core/src/test/java/org/apache/qpid/server/security/group/FileGroupDatabaseTest.java index 0ccbe3c..4a6d269 100644 --- a/broker-core/src/test/java/org/apache/qpid/server/security/group/FileGroupDatabaseTest.java +++ b/broker-core/src/test/java/org/apache/qpid/server/security/group/FileGroupDatabaseTest.java @@ -21,6 +21,7 @@ package org.apache.qpid.server.security.group; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -34,6 +35,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.apache.qpid.server.configuration.IllegalConfigurationException; import org.apache.qpid.server.model.adapter.FileBasedGroupProvider; import org.apache.qpid.test.utils.UnitTestBase; @@ -215,21 +217,12 @@ public class FileGroupDatabaseTest extends UnitTestBase assertTrue(_fileGroupDatabase.getGroupsForUser(USER1.toUpperCase()).isEmpty()); } - @Test + @Test(expected = IllegalConfigurationException.class) public void testGetGroupPrincipalsForUserWhenUserAddedToGroupTheyAreAlreadyIn() throws Exception { _util.writeAndSetGroupFile("myGroup.users", USER1); _fileGroupDatabase.addUserToGroup(USER1, MY_GROUP); - Set<String> groups = _fileGroupDatabase.getGroupsForUser(USER1); - - assertEquals(1, groups.size()); - assertTrue(groups.contains(MY_GROUP)); - - Set<String> users = _fileGroupDatabase.getUsersInGroup(MY_GROUP); - assertEquals(1, users.size()); - assertTrue(users.contains(USER1)); - assertTrue(_fileGroupDatabase.getGroupsForUser(MY_GROUP.toUpperCase()).isEmpty()); } @Test --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org