Skip to content

Commit fd906a8

Browse files
committed
[feature] Allow access to specific Environment Variables when using fn:available-environment-variables#0 and fn:environment-variable#1 to be configured from conf.xml
1 parent d80563d commit fd906a8

File tree

9 files changed

+1248
-19
lines changed

9 files changed

+1248
-19
lines changed

exist-core/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,8 @@
697697
<exclude>src/main/java/org/exist/xquery/Cardinality.java</exclude>
698698
<exclude>src/test/java/org/exist/xquery/ImportModuleTest.java</exclude>
699699
<exclude>src/test/java/org/exist/xquery/XQueryContextAttributesTest.java</exclude>
700+
<exclude>src/main/java/org/exist/xquery/functions/AccessUtil.java</exclude>
701+
<exclude>src/test/java/org/exist/xquery/functions/fn/FunEnvironmentTest.java</exclude>
700702
<exclude>src/main/java/org/exist/xquery/functions/map/MapType.java</exclude>
701703
<exclude>src/test/java/org/exist/xquery/functions/session/AbstractSessionTest.java</exclude>
702704
<exclude>src/test/java/org/exist/xquery/functions/xmldb/AbstractXMLDBTest.java</exclude>
@@ -845,6 +847,8 @@ The original license statement is also included below.]]></preamble>
845847
<include>src/main/java/org/exist/xquery/Cardinality.java</include>
846848
<include>src/test/java/org/exist/xquery/ImportModuleTest.java</include>
847849
<include>src/test/java/org/exist/xquery/XQueryContextAttributesTest.java</include>
850+
<include>src/main/java/org/exist/xquery/functions/AccessUtil.java</include>
851+
<include>src/test/java/org/exist/xquery/functions/fn/FunEnvironmentTest.java</include>
848852
<include>src/main/java/org/exist/xquery/functions/map/MapType.java</include>
849853
<include>src/test/java/org/exist/xquery/functions/session/AbstractSessionTest.java</include>
850854
<include>src/test/java/org/exist/xquery/functions/xmldb/AbstractXMLDBTest.java</include>

exist-core/src/main/java/org/exist/util/Configuration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,6 @@ private void configureXQuery( Element xquery ) throws DatabaseConfigurationExcep
410410
* @throws DatabaseConfigurationException
411411
*/
412412
private void loadModuleClasses( Element xquery, Map<String, Class<?>> modulesClassMap, Map<String, String> modulesSourceMap, Map<String, Map<String, List<? extends Object>>> moduleParameters) throws DatabaseConfigurationException {
413-
// add the standard function module
414-
modulesClassMap.put(Namespaces.XPATH_FUNCTIONS_NS, org.exist.xquery.functions.fn.FnModule.class);
415-
416413
// add other modules specified in configuration
417414
final NodeList builtins = xquery.getElementsByTagName(XQUERY_BUILTIN_MODULES_CONFIGURATION_MODULES_ELEMENT_NAME);
418415

@@ -474,6 +471,11 @@ private void loadModuleClasses( Element xquery, Map<String, Class<?>> modulesCla
474471
}
475472
}
476473
}
474+
475+
// if not specified in the conf.xml, then add the standard function module anyway
476+
if (!modulesClassMap.containsKey(Namespaces.XPATH_FUNCTIONS_NS)) {
477+
modulesClassMap.put(Namespaces.XPATH_FUNCTIONS_NS, org.exist.xquery.functions.fn.FnModule.class);
478+
}
477479
}
478480

479481
/**

exist-core/src/main/java/org/exist/xquery/AbstractInternalModule.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ protected List<? extends Object> getParameter(final String paramName) {
8484
return parameters.get(paramName);
8585
}
8686

87+
/**
88+
* Get the module parameters.
89+
*
90+
* @return the module parameters.
91+
*/
92+
protected Map<String, List<? extends Object>> getParameters() {
93+
return parameters;
94+
}
95+
8796
@Override
8897
public void setContextItem(final Sequence contextItem) {
8998
// not used for internal modules
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* Copyright (C) 2014, Evolved Binary Ltd
3+
*
4+
* This file was originally ported from FusionDB to eXist-db by
5+
* Evolved Binary, for the benefit of the eXist-db Open Source community.
6+
* Only the ported code as it appears in this file, at the time that
7+
* it was contributed to eXist-db, was re-licensed under The GNU
8+
* Lesser General Public License v2.1 only for use in eXist-db.
9+
*
10+
* This license grant applies only to a snapshot of the code as it
11+
* appeared when ported, it does not offer or infer any rights to either
12+
* updates of this source code or access to the original source code.
13+
*
14+
* The GNU Lesser General Public License v2.1 only license follows.
15+
*
16+
* ---------------------------------------------------------------------
17+
*
18+
* Copyright (C) 2014, Evolved Binary Ltd
19+
*
20+
* This library is free software; you can redistribute it and/or
21+
* modify it under the terms of the GNU Lesser General Public
22+
* License as published by the Free Software Foundation; version 2.1.
23+
*
24+
* This library is distributed in the hope that it will be useful,
25+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
26+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27+
* Lesser General Public License for more details.
28+
*
29+
* You should have received a copy of the GNU Lesser General Public
30+
* License along with this library; if not, write to the Free Software
31+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
32+
*/
33+
package org.exist.xquery.functions;
34+
35+
import com.evolvedbinary.j8fu.tuple.Tuple2;
36+
import io.lacuna.bifurcan.IMap;
37+
import io.lacuna.bifurcan.ISet;
38+
import io.lacuna.bifurcan.LinearSet;
39+
import io.lacuna.bifurcan.LinearMap;
40+
import io.lacuna.bifurcan.Map;
41+
import io.lacuna.bifurcan.Set;
42+
import org.exist.security.Subject;
43+
import org.exist.security.internal.SecurityManagerImpl;
44+
45+
import java.util.List;
46+
import java.util.regex.Matcher;
47+
import java.util.regex.Pattern;
48+
49+
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
50+
51+
/**
52+
* @author <a href="mailto:[email protected]">Adam Retter</a>
53+
*/
54+
public class AccessUtil {
55+
56+
static final String OTHERWISE = "*";
57+
58+
/**
59+
* Parse access parameters into Group Access Rules and User Access Rules.
60+
*
61+
* @param accessRulePattern A pattern whose first group matches a "name",
62+
* and whose second pattern matches the String "Group" or "User".
63+
* @param parameters the module parameters.
64+
*
65+
* @return a Tuple where the first entry is the Group Access Rules, and the second entry is the User Access Rules.
66+
*/
67+
public static Tuple2<IMap<String, ISet<String>>, IMap<String, ISet<String>>> parseAccessParameters(
68+
final Pattern accessRulePattern, final java.util.Map<String, List<?>> parameters) {
69+
IMap<String, ISet<String>> accessGroupRules = null;
70+
IMap<String, ISet<String>> accessUserRules = null;
71+
72+
if (parameters != null) {
73+
Matcher matcher = null;
74+
for (final java.util.Map.Entry<String, List<?>> parameter : parameters.entrySet()) {
75+
final String parameterName = parameter.getKey();
76+
if (matcher == null) {
77+
matcher = accessRulePattern.matcher(parameterName);
78+
} else {
79+
matcher.reset(parameterName);
80+
}
81+
82+
if (matcher.matches()) {
83+
final String principalType = matcher.group(2);
84+
if ("Group".equals(principalType)) {
85+
if (accessGroupRules == null) {
86+
accessGroupRules = new LinearMap<>();
87+
}
88+
final String name = matcher.group(1);
89+
accessGroupRules.put(name, toSet(parameter.getValue()));
90+
} else if ("User".equals(principalType)) {
91+
if (accessUserRules == null) {
92+
accessUserRules = new LinearMap<>();
93+
}
94+
final String name = matcher.group(1);
95+
accessUserRules.put(name, toSet(parameter.getValue()));
96+
}
97+
}
98+
}
99+
}
100+
101+
if ((accessGroupRules == null || !accessGroupRules.contains(OTHERWISE))
102+
&& ((accessUserRules == null) || !accessUserRules.contains(OTHERWISE))) {
103+
if (accessGroupRules == null) {
104+
accessGroupRules = new LinearMap<>(1);
105+
ISet<String> otherwiseDba = new LinearSet<>(1);
106+
otherwiseDba.add(SecurityManagerImpl.DBA_GROUP);
107+
otherwiseDba = otherwiseDba.forked();
108+
accessGroupRules.put(OTHERWISE, otherwiseDba);
109+
}
110+
}
111+
112+
if (accessGroupRules == null) {
113+
accessGroupRules = Map.empty();
114+
} else {
115+
accessGroupRules = accessGroupRules.forked();
116+
}
117+
118+
if (accessUserRules == null) {
119+
accessUserRules = Map.empty();
120+
} else {
121+
accessUserRules = accessUserRules.forked();
122+
}
123+
124+
return Tuple(accessGroupRules, accessUserRules);
125+
}
126+
127+
private static ISet<String> toSet(final List<?> values) {
128+
ISet<String> set;
129+
if (values.size() > 0) {
130+
set = new LinearSet<>();
131+
for (final Object value : values) {
132+
set.add(value.toString());
133+
}
134+
set = set.forked();
135+
} else {
136+
set = Set.empty();
137+
}
138+
return set;
139+
}
140+
141+
/**
142+
* Returns true of the user is allowed access to the name.
143+
*
144+
* @param user the user to test the access rules against.
145+
* @param accessGroupRules the group access rules.
146+
* @param accessUserRules the user access rules.
147+
* @param name the name of the resource that the user wishes to access.
148+
*/
149+
public static boolean isAllowedAccess(final Subject user, final IMap<String, ISet<String>> accessGroupRules,
150+
final IMap<String, ISet<String>> accessUserRules, final String name) {
151+
return hasGroupAccess(user, accessGroupRules, name)
152+
|| hasUserAccess(user, accessUserRules, name);
153+
}
154+
155+
private static boolean hasGroupAccess(final Subject user, final IMap<String, ISet<String>> accessGroupRules,
156+
final String name) {
157+
ISet<String> accessGroups = accessGroupRules.get(name, null);
158+
159+
// fallback to "otherwise"
160+
if (accessGroups == null) {
161+
accessGroups = accessGroupRules.get(OTHERWISE, null);
162+
}
163+
164+
if (accessGroups != null) {
165+
final String[] userGroups = user.getGroups();
166+
for (final String userGroup : userGroups) {
167+
if (accessGroups.contains(userGroup)) {
168+
return true;
169+
}
170+
}
171+
}
172+
173+
return false;
174+
}
175+
176+
private static boolean hasUserAccess(final Subject user, final IMap<String, ISet<String>> accessUserRules,
177+
final String name) {
178+
ISet<String> accessUsers = accessUserRules.get(name, null);
179+
180+
// fallback to "otherwise"
181+
if (accessUsers == null) {
182+
accessUsers = accessUserRules.get(OTHERWISE, null);
183+
}
184+
185+
if (accessUsers != null) {
186+
return accessUsers.contains(user.getUsername());
187+
}
188+
189+
return false;
190+
}
191+
}

exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@
2424
import java.util.Arrays;
2525
import java.util.List;
2626
import java.util.Map;
27+
import java.util.regex.Pattern;
2728

29+
import com.evolvedbinary.j8fu.tuple.Tuple2;
30+
import io.lacuna.bifurcan.IMap;
31+
import io.lacuna.bifurcan.ISet;
2832
import org.exist.dom.QName;
33+
import org.exist.util.PatternFactory;
2934
import org.exist.xquery.*;
35+
import org.exist.xquery.functions.AccessUtil;
3036
import org.exist.xquery.value.FunctionParameterSequenceType;
3137
import org.exist.xquery.value.FunctionReturnSequenceType;
3238

@@ -274,7 +280,12 @@ public class FnModule extends AbstractInternalModule {
274280
public final static ErrorCodes.ErrorCode SEPM0019 = new ErrorCodes.ErrorCode("SEPM0019", "It is an error if an instance of the data model " +
275281
"used to specify the settings of serialization parameters specifies the value of the same parameter more than once.");
276282

277-
public FnModule(Map<String, List<?>> parameters) {
283+
private static final Pattern PTN_ENVIRONMENT_VARIABLE_ACCESS = PatternFactory.getInstance().getPattern("environmentVariableAccess\\.([^=\\00])+\\.requires((?:Group)|(?:User))");
284+
285+
private IMap<String, ISet<String>> environmentVariableAccessGroups = null;
286+
private IMap<String, ISet<String>> environmentVariableAccessUsers = null;
287+
288+
public FnModule(final Map<String, List<?>> parameters) {
278289
super(functions, parameters, true);
279290
}
280291

@@ -303,6 +314,34 @@ public String getReleaseVersion() {
303314
return RELEASED_IN_VERSION;
304315
}
305316

317+
/**
318+
* Get the environment variable names and groups that are allowed to access them.
319+
*
320+
* @return a map where the key is the environment variable name, and the value is a set of group names.
321+
*/
322+
IMap<String, ISet<String>> getEnvironmentVariableAccessGroups() {
323+
if (environmentVariableAccessGroups == null) {
324+
final Tuple2<IMap<String, ISet<String>>, IMap<String, ISet<String>>> accessRules = AccessUtil.parseAccessParameters(PTN_ENVIRONMENT_VARIABLE_ACCESS, getParameters());
325+
this.environmentVariableAccessGroups = accessRules._1;
326+
this.environmentVariableAccessUsers = accessRules._2;
327+
}
328+
return environmentVariableAccessGroups;
329+
}
330+
331+
/**
332+
* Get the environment variable names and users that are allowed to access them.
333+
*
334+
* @return a map where the key is the environment variable name, and the value is a set of usernames.
335+
*/
336+
IMap<String, ISet<String>> getEnvironmentVariableAccessUsers() {
337+
if (environmentVariableAccessUsers == null) {
338+
final Tuple2<IMap<String, ISet<String>>, IMap<String, ISet<String>>> accessRules = AccessUtil.parseAccessParameters(PTN_ENVIRONMENT_VARIABLE_ACCESS, getParameters());
339+
this.environmentVariableAccessGroups = accessRules._1;
340+
this.environmentVariableAccessUsers = accessRules._2;
341+
}
342+
return environmentVariableAccessUsers;
343+
}
344+
306345
static FunctionSignature functionSignature(final String name, final String description, final FunctionReturnSequenceType returnType, final FunctionParameterSequenceType... paramTypes) {
307346
return FunctionDSL.functionSignature(new QName(name, Function.BUILTIN_FUNCTION_NS), description, returnType, paramTypes);
308347
}

exist-core/src/main/java/org/exist/xquery/functions/fn/FunEnvironment.java

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.exist.xquery.FunctionSignature;
3333
import org.exist.xquery.XPathException;
3434
import org.exist.xquery.XQueryContext;
35+
import org.exist.xquery.functions.AccessUtil;
3536
import org.exist.xquery.value.FunctionParameterSequenceType;
3637
import org.exist.xquery.value.FunctionReturnSequenceType;
3738
import org.exist.xquery.value.Sequence;
@@ -50,17 +51,19 @@ public class FunEnvironment extends BasicFunction {
5051
"Returns a list of environment variable names.",
5152
null,
5253
new FunctionReturnSequenceType(Type.STRING, Cardinality.ZERO_OR_MORE,
53-
"Returns a sequence of strings, being the names of the environment variables. User must be DBA.")
54+
"Returns a sequence of strings, being the names of the environment variables. " +
55+
"Only the Environment Variables that the calling user has been granted access to are returned (see conf.xml).")
5456
),
5557
new FunctionSignature(
5658
new QName("environment-variable", Function.BUILTIN_FUNCTION_NS),
5759
"Returns the value of a system environment variable, if it exists.",
58-
new SequenceType[]{
60+
new SequenceType[] {
5961
new FunctionParameterSequenceType("name", Type.STRING,
6062
Cardinality.EXACTLY_ONE, "Name of environment variable.")
6163
},
62-
new FunctionReturnSequenceType(Type.STRING, Cardinality.ZERO_OR_ONE, "Corresponding value of the environment variable, "
63-
+ "if there is no environment variable with a matching name, the function returns the empty sequence. User must be DBA.")
64+
new FunctionReturnSequenceType(Type.STRING, Cardinality.ZERO_OR_ONE, "Corresponding value of the environment variable, " +
65+
"if there is no environment variable with a matching name, the function returns the empty sequence. " +
66+
"User must have been granted access to the Environment Variable (see conf.xml).")
6467
)
6568
};
6669

@@ -70,33 +73,33 @@ public FunEnvironment(final XQueryContext context, final FunctionSignature signa
7073

7174
@Override
7275
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
73-
74-
if (!context.getSubject().hasDbaRole()) {
75-
final String txt = "Permission denied, calling user '" + context.getSubject().getName() + "' must be a DBA to call this function.";
76-
LOGGER.error(txt);
77-
return Sequence.EMPTY_SEQUENCE;
78-
}
76+
final FnModule fnModule = (FnModule) getParentModule();
77+
final IMap<String, ISet<String>> environmentVariableAccessGroups = fnModule.getEnvironmentVariableAccessGroups();
78+
final IMap<String, ISet<String>> environmentVariableAccessUsers = fnModule.getEnvironmentVariableAccessUsers();
7979

8080
if (isCalledAs("available-environment-variables")) {
8181

8282
final Sequence result = new ValueSequence();
8383

8484
final IMap<String, String> environmentVariables = context.getEnvironmentVariables();
8585
for (final String environmentVariableName : environmentVariables.keys()) {
86-
result.add(new StringValue(environmentVariableName));
86+
if (AccessUtil.isAllowedAccess(context.getEffectiveUser(), environmentVariableAccessGroups, environmentVariableAccessUsers, environmentVariableName)) {
87+
result.add(new StringValue(environmentVariableName));
88+
}
8789
}
8890

8991
return result;
9092

9193
} else {
9294

93-
if (args[0].isEmpty()) {
95+
final String environmentVariableName = args[0].itemAt(0).getStringValue();
96+
if (!AccessUtil.isAllowedAccess(context.getEffectiveUser(), environmentVariableAccessGroups, environmentVariableAccessUsers, environmentVariableName)) {
97+
final String txt = "Permission denied, calling user '" + context.getSubject().getName() + "' must be granted access to the Environment Variable: " + environmentVariableName + ".";
98+
LOGGER.error(txt);
9499
return Sequence.EMPTY_SEQUENCE;
95100
}
96101

97-
final String parameter = args[0].itemAt(0).getStringValue();
98-
99-
final String value = context.getEnvironmentVariables().get(parameter, null);
102+
final String value = context.getEnvironmentVariables().get(environmentVariableName, null);
100103
if (value == null) {
101104
return Sequence.EMPTY_SEQUENCE;
102105
}

0 commit comments

Comments
 (0)