Skip to content

Commit 012dfac

Browse files
committed
Sandboxing JEXL (#171)
1 parent da8bd88 commit 012dfac

9 files changed

Lines changed: 189 additions & 112 deletions

File tree

core/provisioning-api/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ under the License.
8080
<version>${project.version}</version>
8181
</dependency>
8282

83+
<dependency>
84+
<groupId>org.slf4j</groupId>
85+
<artifactId>slf4j-simple</artifactId>
86+
<version>${slf4j.version}</version>
87+
<scope>test</scope>
88+
</dependency>
8389
<dependency>
8490
<groupId>org.mockito</groupId>
8591
<artifactId>mockito-core</artifactId>

core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/ClassFreeUberspect.java

Lines changed: 0 additions & 41 deletions
This file was deleted.

core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,4 @@ public Class<?> loadClass(final String name) throws ClassNotFoundException {
3232
protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
3333
throw new ClassNotFoundException("This classloader won't attemp to load " + name);
3434
}
35-
3635
}

core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtils.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,15 @@ public final class JexlUtils {
6565
private static final String[] IGNORE_FIELDS = { "password", "clearPassword", "serialVersionUID", "class" };
6666

6767
private static final Map<Class<?>, Set<Pair<PropertyDescriptor, Field>>> FIELD_CACHE =
68-
Collections.<Class<?>, Set<Pair<PropertyDescriptor, Field>>>synchronizedMap(
69-
new HashMap<Class<?>, Set<Pair<PropertyDescriptor, Field>>>());
68+
Collections.synchronizedMap(new HashMap<Class<?>, Set<Pair<PropertyDescriptor, Field>>>());
7069

7170
private static JexlEngine JEXL_ENGINE;
7271

7372
private static JexlEngine getEngine() {
7473
synchronized (LOG) {
7574
if (JEXL_ENGINE == null) {
7675
JEXL_ENGINE = new JexlBuilder().
77-
uberspect(new ClassFreeUberspect()).
76+
uberspect(new SandboxUberspect()).
7877
loader(new EmptyClassLoader()).
7978
namespaces(Map.of("syncope", new SyncopeJexlFunctions())).
8079
cache(512).
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.syncope.core.provisioning.api.jexl;
20+
21+
import java.time.Instant;
22+
import java.util.Arrays;
23+
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.Date;
26+
import java.util.HashSet;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.Optional;
30+
import java.util.Set;
31+
import org.apache.commons.jexl3.JexlEngine;
32+
import org.apache.commons.jexl3.internal.introspection.Uberspect;
33+
import org.apache.commons.jexl3.introspection.JexlMethod;
34+
import org.apache.commons.jexl3.introspection.JexlPropertySet;
35+
import org.apache.commons.jexl3.introspection.JexlUberspect;
36+
import org.apache.commons.logging.LogFactory;
37+
import org.apache.syncope.common.lib.Attr;
38+
import org.apache.syncope.common.lib.to.AnyTO;
39+
import org.apache.syncope.common.lib.to.MembershipTO;
40+
import org.apache.syncope.common.lib.to.RealmTO;
41+
import org.apache.syncope.core.persistence.api.entity.Any;
42+
import org.apache.syncope.core.persistence.api.entity.Membership;
43+
import org.apache.syncope.core.persistence.api.entity.PlainAttr;
44+
import org.apache.syncope.core.persistence.api.entity.Realm;
45+
46+
class SandboxUberspect extends Uberspect {
47+
48+
private static final Set<String> COLLECTION_METHODS = Collections.unmodifiableSet(new HashSet<>(
49+
Arrays.asList("contains", "containsAll", "isEmpty", "size", "iterator", "toString")));
50+
51+
private static final Set<String> LIST_METHODS = Collections.unmodifiableSet(new HashSet<>(
52+
Arrays.asList("get", "indexOf", "lastIndexOf", "toString")));
53+
54+
private static final Set<String> MAP_METHODS = Collections.unmodifiableSet(new HashSet<>(
55+
Arrays.asList("get", "getOrDefault", "containsKey", "containsValue", "toString")));
56+
57+
SandboxUberspect() {
58+
super(LogFactory.getLog(JexlEngine.class), JexlUberspect.JEXL_STRATEGY);
59+
}
60+
61+
@Override
62+
public JexlMethod getConstructor(final Object ctorHandle, final Object... args) {
63+
return null;
64+
}
65+
66+
@Override
67+
public JexlMethod getMethod(final Object obj, final String method, final Object... args) {
68+
if (obj instanceof AnyTO || obj instanceof Any
69+
|| obj instanceof PlainAttr || obj instanceof Attr
70+
|| obj instanceof MembershipTO || obj instanceof Membership
71+
|| obj instanceof Realm || obj instanceof RealmTO) {
72+
73+
return super.getMethod(obj, method, args);
74+
} else if (obj instanceof SyncopeJexlFunctions) {
75+
return super.getMethod(obj, method, args);
76+
} else if (obj instanceof Optional) {
77+
return super.getMethod(obj, method, args);
78+
} else if (obj.getClass().isArray()) {
79+
return super.getMethod(obj, method, args);
80+
} else if (obj instanceof String) {
81+
return super.getMethod(obj, method, args);
82+
} else if (obj instanceof Date || obj instanceof Instant) {
83+
return super.getMethod(obj, method, args);
84+
} else if (obj instanceof Map && MAP_METHODS.contains(method)) {
85+
return super.getMethod(obj, method, args);
86+
} else if (obj instanceof List && (LIST_METHODS.contains(method) || COLLECTION_METHODS.contains(method))) {
87+
return super.getMethod(obj, method, args);
88+
} else if (obj instanceof Collection && COLLECTION_METHODS.contains(method)) {
89+
return super.getMethod(obj, method, args);
90+
}
91+
return null;
92+
}
93+
94+
@Override
95+
public JexlPropertySet getPropertySet(final Object obj, final Object identifier, final Object arg) {
96+
return null;
97+
}
98+
99+
@Override
100+
public JexlPropertySet getPropertySet(
101+
final List<PropertyResolver> resolvers, final Object obj, final Object identifier, final Object arg) {
102+
103+
return null;
104+
}
105+
}

core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/jexl/MailTemplateTest.java renamed to core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MailTemplateTest.java

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
package org.apache.syncope.core.provisioning.java.jexl;
20-
21-
import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
19+
package org.apache.syncope.core.provisioning.api.jexl;
2220

2321
import static org.junit.jupiter.api.Assertions.assertFalse;
2422
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -36,17 +34,36 @@
3634
import org.apache.syncope.common.lib.Attr;
3735
import org.apache.syncope.common.lib.to.MembershipTO;
3836
import org.apache.syncope.common.lib.to.UserTO;
39-
import org.apache.syncope.core.persistence.api.dao.MailTemplateDAO;
40-
import org.apache.syncope.core.provisioning.java.AbstractTest;
37+
import org.apache.syncope.core.provisioning.api.AbstractTest;
4138
import org.junit.jupiter.api.Test;
42-
import org.springframework.beans.factory.annotation.Autowired;
43-
import org.springframework.transaction.annotation.Transactional;
4439

45-
@Transactional("Master")
4640
public class MailTemplateTest extends AbstractTest {
4741

48-
@Autowired
49-
private MailTemplateDAO mailTemplateDAO;
42+
private static final String CONFIRM_PASSWORD_RESET_TEMPLATE =
43+
"<html><body>"
44+
+ "<p>Hi,<br/> we are happy to inform you that the password request was successfully executed for "
45+
+ "your account.</p> <p>Best regards.</p> </body> </html>";
46+
47+
private static final String REQUEST_PASSWORD_RESET_TEMPLATE = "Hi, a password reset was request for "
48+
+ "${user.getUsername()}. In order to complete this request, you need to visit this link: "
49+
+ "http://localhost:9080/syncope-enduser/app/#!/confirmpasswordreset?token="
50+
+ "${input.get(0).replaceAll(' ', '%20')}"
51+
+ "If you did not request this reset, just ignore the present e-mail. Best regards.";
52+
53+
private static final String OPTIN_TEMPLATE =
54+
"<html> <body> <h3>Hi ${user.getPlainAttr(\"firstname\").get().values[0]} "
55+
+ "${user.getPlainAttr(\"surname\").get().values[0]}, welcome to Syncope!</h3>"
56+
+ "<p> Your username is ${user.username}.<br/>"
57+
+ "Your email address is ${user.getPlainAttr(\"email\").get().values[0]}."
58+
+ "Your email address inside a <a href=\"http://localhost/?email="
59+
+ "${user.getPlainAttr(\"email\").get().values[0].replace('@', '%40')}\">link</a>.</p>"
60+
+ "<p>This message was sent to the following recipients: <ul>\n $$ for (recipient: recipients) {\n"
61+
+ " <li>${recipient.getPlainAttr(\"email\").get().values[0]}</li>\n $$ }\n </ul>\n"
62+
+ " because one of the following events occurred: <ul>\n $$ for (event: events) {\n"
63+
+ " <li>${event}</li>\n $$ }\n </ul>\n </p> \n $$ if (!empty(user.memberships)) {\n"
64+
+ " You have been provided with the following groups:\n <ul>\n"
65+
+ " $$ for(membership : user.memberships) {\n <li>${membership.groupName}</li>\n $$ }\n"
66+
+ " </ul>\n $$ }\n </body> </html>";
5067

5168
private static String evaluate(final String template, final Map<String, Object> jexlVars) {
5269
StringWriter writer = new StringWriter();
@@ -58,10 +75,7 @@ private static String evaluate(final String template, final Map<String, Object>
5875

5976
@Test
6077
public void confirmPasswordReset() throws IOException {
61-
String htmlBody = evaluate(
62-
mailTemplateDAO.find("confirmPasswordReset").getHTMLTemplate(),
63-
new HashMap<>());
64-
78+
String htmlBody = evaluate(CONFIRM_PASSWORD_RESET_TEMPLATE, new HashMap<>());
6579
assertNotNull(htmlBody);
6680
}
6781

@@ -79,17 +93,15 @@ public void requestPasswordReset() throws IOException {
7993
input.add(token);
8094
ctx.put("input", input);
8195

82-
String htmlBody = evaluate(
83-
mailTemplateDAO.find("requestPasswordReset").getHTMLTemplate(),
84-
ctx);
96+
String textBody = evaluate(REQUEST_PASSWORD_RESET_TEMPLATE, ctx);
8597

86-
assertNotNull(htmlBody);
87-
assertTrue(htmlBody.contains("a password reset was request for " + username + '.'));
88-
assertFalse(htmlBody.contains(
89-
"http://localhost:9080/syncope-enduser/confirmpasswordreset?token="
98+
assertNotNull(textBody);
99+
assertTrue(textBody.contains("a password reset was request for " + username + "."));
100+
assertFalse(textBody.contains(
101+
"http://localhost:9080/syncope-enduser/app/#!/confirmpasswordreset?token="
90102
+ token));
91-
assertTrue(htmlBody.contains(
92-
"http://localhost:9080/syncope-enduser/confirmpasswordreset?token="
103+
assertTrue(textBody.contains(
104+
"http://localhost:9080/syncope-enduser/app/#!/confirmpasswordreset?token="
93105
+ token.replaceAll(" ", "%20")));
94106
}
95107

@@ -115,15 +127,16 @@ public void optin() throws IOException {
115127
recipient.getPlainAttr("email").get().getValues().set(0, "another@syncope.apache.org");
116128
ctx.put("recipients", List.of(recipient));
117129

118-
String htmlBody = evaluate(
119-
mailTemplateDAO.find("optin").getHTMLTemplate(),
120-
ctx);
130+
ctx.put("events", List.of("event1"));
131+
132+
String htmlBody = evaluate(OPTIN_TEMPLATE, ctx);
121133

122134
assertNotNull(htmlBody);
123135

124136
assertTrue(htmlBody.contains("Hi John Doe,"));
125137
assertTrue(htmlBody.contains("Your email address is john.doe@syncope.apache.org."));
126138
assertTrue(htmlBody.contains("<li>another@syncope.apache.org</li>"));
127139
assertTrue(htmlBody.contains("<li>a group</li>"));
140+
assertTrue(htmlBody.contains("<li>event1</li>"));
128141
}
129142
}

core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/jexl/MappingTest.java renamed to core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MappingTest.java

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,71 +16,46 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
package org.apache.syncope.core.provisioning.java.jexl;
20-
21-
import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
19+
package org.apache.syncope.core.provisioning.api.jexl;
2220

2321
import static org.junit.jupiter.api.Assertions.assertEquals;
2422
import static org.junit.jupiter.api.Assertions.assertNotNull;
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.when;
2525

2626
import org.apache.commons.jexl3.JexlContext;
2727
import org.apache.commons.jexl3.MapContext;
28-
import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
29-
import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
30-
import org.apache.syncope.core.persistence.api.dao.RealmDAO;
31-
import org.apache.syncope.core.persistence.api.dao.UserDAO;
3228
import org.apache.syncope.core.persistence.api.entity.Realm;
33-
import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
34-
import org.apache.syncope.core.persistence.api.entity.resource.Provision;
3529
import org.apache.syncope.core.persistence.api.entity.user.User;
36-
import org.apache.syncope.core.provisioning.java.AbstractTest;
30+
import org.apache.syncope.core.provisioning.api.AbstractTest;
3731
import org.junit.jupiter.api.Test;
38-
import org.springframework.beans.factory.annotation.Autowired;
39-
import org.springframework.transaction.annotation.Transactional;
4032

41-
@Transactional("Master")
4233
public class MappingTest extends AbstractTest {
4334

44-
@Autowired
45-
private ExternalResourceDAO resourceDAO;
46-
47-
@Autowired
48-
private AnyTypeDAO anyTypeDAO;
49-
50-
@Autowired
51-
private RealmDAO realmDAO;
52-
53-
@Autowired
54-
private UserDAO userDAO;
55-
5635
@Test
5736
public void anyConnObjectLink() {
58-
ExternalResource ldap = resourceDAO.find("resource-ldap");
59-
assertNotNull(ldap);
60-
61-
Provision provision = ldap.getProvision(anyTypeDAO.findUser()).get();
62-
assertNotNull(provision);
63-
assertNotNull(provision.getMapping());
64-
assertNotNull(provision.getMapping().getConnObjectLink());
37+
Realm realm = mock(Realm.class);
38+
when(realm.getFullPath()).thenReturn("/even");
6539

66-
User user = userDAO.findByUsername("rossini");
40+
User user = mock(User.class);
41+
when(user.getUsername()).thenReturn("rossini");
42+
when(user.getRealm()).thenReturn(realm);
6743
assertNotNull(user);
6844

6945
JexlContext jexlContext = new MapContext();
7046
JexlUtils.addFieldsToContext(user, jexlContext);
71-
JexlUtils.addPlainAttrsToContext(user.getPlainAttrs(), jexlContext);
7247

73-
assertEquals(
74-
"uid=rossini,ou=people,o=isp",
75-
JexlUtils.evaluate(provision.getMapping().getConnObjectLink(), jexlContext));
48+
String connObjectLink = "'uid=' + username + ',ou=people,o=isp'";
49+
assertEquals("uid=rossini,ou=people,o=isp", JexlUtils.evaluate(connObjectLink, jexlContext));
7650

77-
String connObjectLink = "'uid=' + username + realm.replaceAll('/', ',o=') + ',ou=people,o=isp'";
51+
connObjectLink = "'uid=' + username + realm.replaceAll('/', ',o=') + ',ou=people,o=isp'";
7852
assertEquals("uid=rossini,o=even,ou=people,o=isp", JexlUtils.evaluate(connObjectLink, jexlContext));
7953
}
8054

8155
@Test
8256
public void realmConnObjectLink() {
83-
Realm realm = realmDAO.findByFullPath("/even/two");
57+
Realm realm = mock(Realm.class);
58+
when(realm.getFullPath()).thenReturn("/even/two");
8459
assertNotNull(realm);
8560

8661
JexlContext jexlContext = new MapContext();
@@ -89,7 +64,7 @@ public void realmConnObjectLink() {
8964
String connObjectLink = "syncope:fullPath2Dn(fullPath, 'ou') + ',o=isp'";
9065
assertEquals("ou=two,ou=even,o=isp", JexlUtils.evaluate(connObjectLink, jexlContext));
9166

92-
realm = realmDAO.findByFullPath("/even");
67+
when(realm.getFullPath()).thenReturn("/even");
9368
assertNotNull(realm);
9469

9570
jexlContext = new MapContext();

0 commit comments

Comments
 (0)