diff --git a/elemental-parent/pom.xml b/elemental-parent/pom.xml
index 68a31ad942..015b2c0f92 100644
--- a/elemental-parent/pom.xml
+++ b/elemental-parent/pom.xml
@@ -322,6 +322,7 @@
SLASHSTAR_STYLE
SLASHSTAR_STYLE
+ SCRIPT_STYLE
XML_STYLE
SCRIPT_STYLE
XML_STYLE
diff --git a/exist-core/pom.xml b/exist-core/pom.xml
index 5a8a49665f..b3342b02a2 100644
--- a/exist-core/pom.xml
+++ b/exist-core/pom.xml
@@ -883,6 +883,7 @@
src/test/java/org/exist/collections/triggers/XQueryTriggerSetUidTest.java
src/test/java/org/exist/collections/triggers/XQueryTriggerTest.java
src/main/java/org/exist/config/Configuration.java
+ src/main/java/org/exist/config/ConfigurationDocumentTrigger.java
src/main/java/org/exist/config/ConfigurationImpl.java
src/main/java/org/exist/config/Configurator.java
src/test/java/org/exist/config/TwoDatabasesTest.java
@@ -1003,10 +1004,14 @@
src/main/java/org/exist/repo/ExistRepository.java
src/main/java/org/exist/scheduler/UserXQueryJob.java
src/main/java/org/exist/scheduler/impl/QuartzSchedulerImpl.java
+ src/main/java/org/exist/security/AbstractRealm.java
+ src/main/java/org/exist/security/AXSchemaType.java
src/main/java/org/exist/security/EffectiveSubject.java
+ src/main/java/org/exist/security/EXistSchemaType.java
src/test/java/org/exist/security/FnDocSecurityTest.java
src/main/java/org/exist/security/Permission.java
src/main/java/org/exist/security/PermissionRequired.java
+ src/main/java/org/exist/security/Principal.java
src/test/java/org/exist/security/RestApiSecurityTest.java
src/main/java/org/exist/security/SecurityManager.java
src/main/java/org/exist/security/SimpleACLPermissionInternal.java
@@ -1015,7 +1020,11 @@
src/test/java/org/exist/security/UnixStylePermissionTest.java
src/test/java/org/exist/security/XqueryApiTest.java
src/main/java/org/exist/security/internal/AccountImpl.java
+ src/main/java/org/exist/security/internal/RealmImpl.java
+ src/main/java/org/exist/security/internal/SecurityManagerImpl.java
src/main/java/org/exist/security/internal/aider/UnixStylePermissionAider.java
+ src/main/java/org/exist/security/management/AccountsManagement.java
+ src/main/java/org/exist/security/management/GroupsManagement.java
src/main/java/org/exist/source/Source.java
src/main/java/org/exist/source/SourceFactory.java
src/main/java/org/exist/source/URLSource.java
@@ -1587,6 +1596,7 @@
src/test/java/org/exist/collections/triggers/XQueryTriggerSetUidTest.java
src/test/java/org/exist/collections/triggers/XQueryTriggerTest.java
src/main/java/org/exist/config/Configuration.java
+ src/main/java/org/exist/config/ConfigurationDocumentTrigger.java
src/main/java/org/exist/config/ConfigurationImpl.java
src/main/java/org/exist/config/Configurator.java
src/test/java/org/exist/config/TwoDatabasesTest.java
@@ -1722,11 +1732,15 @@
src/main/java/org/exist/resolver/XercesXmlResolverAdapter.java
src/main/java/org/exist/scheduler/UserXQueryJob.java
src/main/java/org/exist/scheduler/impl/QuartzSchedulerImpl.java
+ src/main/java/org/exist/security/AbstractRealm.java
+ src/main/java/org/exist/security/AXSchemaType.java
src/main/java/org/exist/security/EffectiveSubject.java
+ src/main/java/org/exist/security/EXistSchemaType.java
src/test/java/org/exist/security/FnDocSecurityTest.java
src/main/java/org/exist/security/Permission.java
src/main/java/org/exist/security/PermissionRequired.java
src/main/java/org/exist/security/PermissionRequiredCheck.java
+ src/main/java/org/exist/security/Principal.java
src/test/java/org/exist/security/RestApiSecurityTest.java
src/main/java/org/exist/security/SecurityManager.java
src/main/java/org/exist/security/SimpleACLPermissionInternal.java
@@ -1735,7 +1749,11 @@
src/test/java/org/exist/security/UnixStylePermissionTest.java
src/test/java/org/exist/security/XqueryApiTest.java
src/main/java/org/exist/security/internal/AccountImpl.java
+ src/main/java/org/exist/security/internal/RealmImpl.java
+ src/main/java/org/exist/security/internal/SecurityManagerImpl.java
src/main/java/org/exist/security/internal/aider/UnixStylePermissionAider.java
+ src/main/java/org/exist/security/management/AccountsManagement.java
+ src/main/java/org/exist/security/management/GroupsManagement.java
src/main/java/org/exist/source/Source.java
src/main/java/org/exist/source/SourceFactory.java
src/main/java/org/exist/source/URLSource.java
diff --git a/exist-core/src/main/java/org/exist/config/ConfigurationDocumentTrigger.java b/exist-core/src/main/java/org/exist/config/ConfigurationDocumentTrigger.java
index 328a556941..6126e90a7e 100644
--- a/exist-core/src/main/java/org/exist/config/ConfigurationDocumentTrigger.java
+++ b/exist-core/src/main/java/org/exist/config/ConfigurationDocumentTrigger.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -332,9 +356,10 @@ private void processPrincipal(final PrincipalType principalType) throws SAXExcep
//check if there is a name collision, i.e. another principal with the same name
final String principalName = findName();
- // first check if the account or group exists before trying to retrieve it
- // otherwise the LDAP realm will create a new user, leading to an endless loop
+
+ // NOTE(AR) first check if the account or group exists before trying to retrieve it otherwise a realm (e.g. LDAP) may create a new user, which could lead to an endless loop and eventually a StackOverflowError
final boolean principalExists = principalName != null && principalType.hasPrincipal(sm, principalName);
+
Principal existingPrincipleByName = null;
if (principalExists) {
existingPrincipleByName = principalType.getPrincipal(sm, principalName);
diff --git a/exist-core/src/main/java/org/exist/config/Configurator.java b/exist-core/src/main/java/org/exist/config/Configurator.java
index ada04750a9..391d6d463a 100644
--- a/exist-core/src/main/java/org/exist/config/Configurator.java
+++ b/exist-core/src/main/java/org/exist/config/Configurator.java
@@ -66,6 +66,7 @@
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import javax.annotation.Nullable;
import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
@@ -156,9 +157,15 @@ protected static T getAnnotation(Field field, Class an
*
* @return The Getter method for the property or null
*/
- public static Method searchForGetMethod(final Class> clazz, final String property) {
+ public static @Nullable Method searchForGetMethod(final Class> clazz, final String property) {
try {
- final String methodName = ("get" + property).toLowerCase();
+ String methodName = property;
+ if (!methodName.startsWith("get-") && !methodName.startsWith("get_")) {
+ methodName = "get" + methodName;
+ }
+ methodName = methodName.replace("-", "");
+ methodName = methodName.replace("_", "");
+
for (final Method method : clazz.getMethods()) {
if (method.getName().equalsIgnoreCase(methodName)) {
return method;
@@ -178,9 +185,15 @@ public static Method searchForGetMethod(final Class> clazz, final String prope
*
* @return The Setter method for the field or null
*/
- public static Method searchForSetMethod(final Class> clazz, final Field field) {
+ public static @Nullable Method searchForSetMethod(final Class> clazz, final Field field) {
try {
- final String methodName = ("set" + field.getName()).toLowerCase();
+ String methodName = field.getName();
+ if (!methodName.startsWith("set-") && !methodName.startsWith("set_")) {
+ methodName = "set" + methodName;
+ }
+ methodName = methodName.replace("-", "");
+ methodName = methodName.replace("_", "");
+
for (final Method method : clazz.getMethods()) {
if (method.getName().equalsIgnoreCase(methodName)) {
return method;
@@ -200,9 +213,14 @@ public static Method searchForSetMethod(final Class> clazz, final Field field)
*
* @return The Adder method for the property or null
*/
- public static Method searchForAddMethod(final Class> clazz, final String property) {
+ public static @Nullable Method searchForAddMethod(final Class> clazz, final String property) {
try {
- final String methodName = ("add" + property).toLowerCase();
+ String methodName = property;
+ if (!methodName.startsWith("add-") && !methodName.startsWith("add_")) {
+ methodName = "add" + methodName;
+ }
+ methodName = methodName.replace("-", "");
+ methodName = methodName.replace("_", "");
for (final Method method : clazz.getMethods()) {
if (method.getName().equalsIgnoreCase(methodName)
&& method.getParameterTypes().length == 1
@@ -216,9 +234,15 @@ public static Method searchForAddMethod(final Class> clazz, final String prope
return null;
}
- public static Method searchForInsertMethod(final Class> clazz, final String property) {
+ public static @Nullable Method searchForInsertMethod(final Class> clazz, final String property) {
try {
- final String methodName = ("insert" + property).toLowerCase();
+ String methodName = property;
+ if (!methodName.startsWith("insert-") && !methodName.startsWith("insert_")) {
+ methodName = "insert" + methodName;
+ }
+ methodName = methodName.replace("-", "");
+ methodName = methodName.replace("_", "");
+
for (final Method method : clazz.getMethods()) {
if (method.getName().equalsIgnoreCase(methodName)
&& method.getParameterTypes().length == 2
diff --git a/exist-core/src/main/java/org/exist/security/AXSchemaType.java b/exist-core/src/main/java/org/exist/security/AXSchemaType.java
index 59fb6b5e09..305a77c614 100644
--- a/exist-core/src/main/java/org/exist/security/AXSchemaType.java
+++ b/exist-core/src/main/java/org/exist/security/AXSchemaType.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -21,6 +45,8 @@
*/
package org.exist.security;
+import javax.annotation.Nullable;
+
/**
*
* @author Adam Retter
@@ -54,7 +80,7 @@ public String getAlias() {
return alias;
}
- public static AXSchemaType valueOfNamespace(final String namespace) {
+ public static @Nullable AXSchemaType valueOfNamespace(final String namespace) {
for(final AXSchemaType axSchemaType : AXSchemaType.values()) {
if(axSchemaType.getNamespace().equals(namespace)) {
return axSchemaType;
@@ -63,7 +89,7 @@ public static AXSchemaType valueOfNamespace(final String namespace) {
return null;
}
- public static AXSchemaType valueOfAlias(final String alias) {
+ public static @Nullable AXSchemaType valueOfAlias(final String alias) {
for(final AXSchemaType axSchemaType : AXSchemaType.values()) {
if(axSchemaType.getAlias().equals(alias)) {
return axSchemaType;
diff --git a/exist-core/src/main/java/org/exist/security/AbstractRealm.java b/exist-core/src/main/java/org/exist/security/AbstractRealm.java
index 8f14f1538b..e038911693 100644
--- a/exist-core/src/main/java/org/exist/security/AbstractRealm.java
+++ b/exist-core/src/main/java/org/exist/security/AbstractRealm.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -22,12 +46,15 @@
package org.exist.security;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import com.evolvedbinary.j8fu.tuple.Tuple2;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.Database;
@@ -49,6 +76,10 @@
import org.exist.util.LockException;
import org.exist.xmldb.XmldbURI;
+import javax.annotation.Nullable;
+
+import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
+
/**
* @author Dmitriy Shabanov
*
@@ -117,7 +148,7 @@ private void loadGroupsFromRealmStorage(final DBBroker broker) throws Configurat
final GroupImpl group = new GroupImpl(r, conf);
getSecurityManager().registerGroup(group);
- principalDb.put(group.getName(), group);
+ principalDb.put(group.getName(), Tuple(PrincipalState.PERSISTENT, Optional.of(group)));
//set collection
if(group.getId() > 0) {
@@ -181,7 +212,7 @@ private void loadAccountsFromRealmStorage(final DBBroker broker) throws Configur
}
getSecurityManager().registerAccount(account);
- principalDb.put(account.getName(), account);
+ principalDb.put(account.getName(), Tuple(PrincipalState.PERSISTENT, Optional.of(account)));
//set collection
if(account.getId() > 0) {
@@ -252,34 +283,67 @@ public void save() throws PermissionDeniedException, EXistException {
configuration.save();
}
+ public final void registerAccountCreation(final String name) {
+ usersByName.write(principalDb -> {
+ @Nullable final Tuple2> accountEntry = principalDb.get(name);
+ if (accountEntry != null) {
+ // if the account is already being created or already persisted then we don't need to do anything
+ return;
+ }
+
+ principalDb.put(name, Tuple(PrincipalState.CREATING, Optional.empty()));
+ });
+ }
+
//Accounts management methods
public final Account registerAccount(final Account account) {
usersByName.write(principalDb -> {
- if(principalDb.containsKey(account.getName())) {
- throw new IllegalArgumentException("Account " + account.getName() + " exist.");
+ @Nullable final Tuple2> accountEntry = principalDb.get(account.getName());
+ if (accountEntry != null && accountEntry._1 == PrincipalState.PERSISTENT) {
+ throw new IllegalArgumentException("The Account " + account.getName() + " already exists.");
}
- principalDb.put(account.getName(), account);
+ principalDb.put(account.getName(), Tuple(PrincipalState.PERSISTENT, Optional.of(account)));
});
return account;
}
+
+ public final void registerGroupCreation(final String name) {
+ groupsByName.write(principalDb -> {
+ @Nullable final Tuple2> accountEntry = principalDb.get(name);
+ if (accountEntry != null) {
+ // if the group is already being created or already persisted then we don't need to do anything
+ return;
+ }
+
+ principalDb.put(name, Tuple(PrincipalState.CREATING, Optional.empty()));
+ });
+ }
public final Group registerGroup(final Group group) {
groupsByName.write(principalDb -> {
- if(principalDb.containsKey(group.getName())) {
- throw new IllegalArgumentException("Group " + group.getName() + " already exists.");
+ @Nullable final Tuple2> groupEntry = principalDb.get(group.getName());
+ if (groupEntry != null && groupEntry._1 == PrincipalState.PERSISTENT) {
+ throw new IllegalArgumentException("The Group " + group.getName() + " already exists.");
}
- principalDb.put(group.getName(), group);
+ principalDb.put(group.getName(), Tuple(PrincipalState.PERSISTENT, Optional.of(group)));
});
return group;
}
@Override
- public Account getAccount(final String name) {
- return usersByName.read(principalDb -> principalDb.get(name));
+ public @Nullable Account getAccount(final String name) {
+ return usersByName.read(principalDb -> {
+ @Nullable final Tuple2> principalEntry = principalDb.get(name);
+ if (principalEntry == null || principalEntry._1 != PrincipalState.PERSISTENT) {
+ // skip non-existent and not yet persisted entries
+ return null;
+ }
+ return principalEntry._2.get();
+ });
}
@Override
@@ -288,7 +352,7 @@ public boolean hasAccount(final String accountName) {
}
@Override
- public final boolean hasAccount(final Account account) {
+ public boolean hasAccount(final Account account) {
return hasAccountLocal(account);
}
@@ -299,12 +363,33 @@ public boolean hasAccountLocal(final Account account) {
@Override
public boolean hasAccountLocal(final String accountName) {
- return usersByName.read(principalDb -> principalDb.containsKey(accountName));
+ return usersByName.read(principalDb -> {
+ @Nullable final Tuple2> principalEntry = principalDb.get(accountName);
+ // skip non-existent and not yet persisted entries
+ return principalEntry != null && principalEntry._1 == PrincipalState.PERSISTENT;
+ });
}
@Override
public final java.util.Collection getAccounts() {
- return usersByName.read(Map::values);
+ return usersByName.read(principalDb -> {
+ @Nullable List accounts = null;
+ for (final Tuple2> principalEntry : principalDb.values()) {
+ // skip not yet persisted entries
+ if (principalEntry._1 == PrincipalState.PERSISTENT) {
+ if (accounts == null) {
+ accounts = new ArrayList<>();
+ }
+ accounts.add(principalEntry._2.get());
+ }
+ }
+
+ if (accounts == null) {
+ accounts = Collections.emptyList();
+ }
+
+ return accounts;
+ });
}
//Groups management methods
@@ -321,7 +406,11 @@ public boolean hasGroup(final String name) {
@Override
public boolean hasGroupLocal(final String groupName) {
- return groupsByName.read(principalDb -> principalDb.containsKey(groupName));
+ return groupsByName.read(principalDb -> {
+ @Nullable final Tuple2> principalEntry = principalDb.get(groupName);
+ // skip not yet persisted entries
+ return principalEntry != null && principalEntry._1 == PrincipalState.PERSISTENT;
+ });
}
@Override
@@ -330,13 +419,37 @@ public final boolean hasGroupLocal(final Group role) {
}
@Override
- public Group getGroup(final String name) {
- return groupsByName.read(principalDb -> principalDb.get(name));
+ public @Nullable Group getGroup(final String name) {
+ return groupsByName.read(principalDb -> {
+ @Nullable final Tuple2> principalEntry = principalDb.get(name);
+ if (principalEntry == null || principalEntry._1 != PrincipalState.PERSISTENT) {
+ // skip non-existent and not yet persisted entries
+ return null;
+ }
+ return principalEntry._2.get();
+ });
}
@Override
public final java.util.Collection getGroups() {
- return groupsByName.read(Map::values);
+ return groupsByName.read(principalDb -> {
+ @Nullable List groups = null;
+ for (final Tuple2> principalEntry : principalDb.values()) {
+ // skip not yet persisted entries
+ if (principalEntry._1 == PrincipalState.PERSISTENT) {
+ if (groups == null) {
+ groups = new ArrayList<>();
+ }
+ groups.add(principalEntry._2.get());
+ }
+ }
+
+ if (groups == null) {
+ groups = Collections.emptyList();
+ }
+
+ return groups;
+ });
}
//collections related methods
@@ -476,7 +589,7 @@ public Group getExternalGroup(final String name) {
//configuration methods
@Override
public boolean isConfigured() {
- return (configuration != null);
+ return configuration != null;
}
@Override
@@ -523,8 +636,24 @@ public java.util.Collection extends String> findGroupnamesWhereGroupnameStarts
public java.util.Collection extends String> findGroupnamesWhereGroupnameContains(final String fragment) {
return Collections.emptyList();
}
-
- protected static class PrincipalDbByName extends ConcurrentValueWrapper