Skip to content

Commit 115de22

Browse files
kumarguandrross
andauthored
Add a policy parser for Java Agent (#17753)
* Add a policy parser for java agent Signed-off-by: Gulshan <[email protected]> * Url no depricated version of url resolution Signed-off-by: Gulshan <[email protected]> * Remove unused methods and switch to modern Java collections Signed-off-by: Gulshan <[email protected]> * Use record classes and other small refactorings Signed-off-by: Andrew Ross <[email protected]> --------- Signed-off-by: Gulshan <[email protected]> Signed-off-by: Andrew Ross <[email protected]> Co-authored-by: Andrew Ross <[email protected]>
1 parent b0bfdc7 commit 115de22

File tree

14 files changed

+880
-0
lines changed

14 files changed

+880
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1818
- [Security Manager Replacement] Create initial Java Agent to intercept Socket::connect calls ([#17724](https://github.com/opensearch-project/OpenSearch/pull/17724))
1919
- Add ingestion management APIs for pause, resume and get ingestion state ([#17631](https://github.com/opensearch-project/OpenSearch/pull/17631))
2020
- [Security Manager Replacement] Enhance Java Agent to intercept System::exit ([#17746](https://github.com/opensearch-project/OpenSearch/pull/17746))
21+
- [Security Manager Replacement] Add a policy parser for Java agent security policies ([#17753](https://github.com/opensearch-project/OpenSearch/pull/17753))
2122
- [Security Manager Replacement] Implement File Interceptor and add integration tests ([#17760](https://github.com/opensearch-project/OpenSearch/pull/17760))
2223
- [Security Manager Replacement] Enhance Java Agent to intercept Runtime::halt ([#17757](https://github.com/opensearch-project/OpenSearch/pull/17757))
2324
- Support AutoExpand for SearchReplica ([#17741](https://github.com/opensearch-project/OpenSearch/pull/17741))

gradle/missing-javadoc.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ configure([
106106
project(":libs:opensearch-secure-sm"),
107107
project(":libs:opensearch-ssl-config"),
108108
project(":libs:opensearch-x-content"),
109+
project(":libs:agent-sm:agent-policy"),
109110
project(":modules:aggs-matrix-stats"),
110111
project(":modules:analysis-common"),
111112
project(":modules:geo"),
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*
8+
* Modifications Copyright OpenSearch Contributors. See
9+
* GitHub history for details.
10+
*/
11+
12+
apply plugin: 'opensearch.build'
13+
apply plugin: 'opensearch.publish'
14+
15+
ext {
16+
failOnJavadocWarning = false
17+
}
18+
19+
base {
20+
archivesName = 'opensearch-agent-policy'
21+
}
22+
23+
disableTasks('forbiddenApisMain')
24+
25+
dependencies {
26+
testImplementation(project(":test:framework"))
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
package org.opensearch.secure_sm.policy;
9+
10+
import java.util.List;
11+
12+
public record GrantEntry(String codeBase, List<PermissionEntry> permissionEntries) {
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
package org.opensearch.secure_sm.policy;
9+
10+
public record PermissionEntry(String permission, String name, String action) {
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.secure_sm.policy;
10+
11+
import java.io.File;
12+
import java.io.FileInputStream;
13+
import java.io.FilePermission;
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.io.InputStreamReader;
17+
import java.net.MalformedURLException;
18+
import java.net.NetPermission;
19+
import java.net.SocketPermission;
20+
import java.net.URI;
21+
import java.net.URISyntaxException;
22+
import java.net.URL;
23+
import java.net.URLDecoder;
24+
import java.nio.charset.StandardCharsets;
25+
import java.security.AllPermission;
26+
import java.security.CodeSource;
27+
import java.security.Permission;
28+
import java.security.PermissionCollection;
29+
import java.security.Permissions;
30+
import java.security.ProtectionDomain;
31+
import java.security.SecurityPermission;
32+
import java.security.cert.Certificate;
33+
import java.util.ArrayList;
34+
import java.util.Enumeration;
35+
import java.util.List;
36+
import java.util.Optional;
37+
import java.util.PropertyPermission;
38+
import java.util.Set;
39+
40+
@SuppressWarnings("removal")
41+
public class PolicyFile extends java.security.Policy {
42+
public static final Set<String> PERM_CLASSES_TO_SKIP = Set.of(
43+
"org.opensearch.secure_sm.ThreadContextPermission",
44+
"org.opensearch.secure_sm.ThreadPermission",
45+
"org.opensearch.SpecialPermission",
46+
"org.bouncycastle.crypto.CryptoServicesPermission",
47+
"org.opensearch.script.ClassPermission",
48+
"javax.security.auth.AuthPermission",
49+
"javax.security.auth.kerberos.ServicePermission"
50+
);
51+
52+
private final PolicyInfo policyInfo;
53+
private final URL url;
54+
55+
public PolicyFile(URL url) {
56+
this.url = url;
57+
try {
58+
policyInfo = init(url);
59+
} catch (PolicyInitializationException e) {
60+
throw new RuntimeException("Failed to initialize policy file", e);
61+
}
62+
}
63+
64+
private PolicyInfo init(URL policy) throws PolicyInitializationException {
65+
PolicyInfo info = new PolicyInfo();
66+
try (InputStreamReader reader = new InputStreamReader(getInputStream(policy), StandardCharsets.UTF_8)) {
67+
List<GrantEntry> grantEntries = PolicyParser.read(reader);
68+
for (GrantEntry grantEntry : grantEntries) {
69+
addGrantEntry(grantEntry, info);
70+
}
71+
} catch (Exception e) {
72+
throw new PolicyInitializationException("Failed to load policy from: " + policy, e);
73+
}
74+
return info;
75+
}
76+
77+
public static InputStream getInputStream(URL url) throws IOException {
78+
if ("file".equals(url.getProtocol())) {
79+
String path = url.getFile().replace('/', File.separatorChar);
80+
path = URLDecoder.decode(path, StandardCharsets.UTF_8);
81+
return new FileInputStream(path);
82+
} else {
83+
return url.openStream();
84+
}
85+
}
86+
87+
private CodeSource getCodeSource(GrantEntry grantEntry) throws PolicyInitializationException {
88+
try {
89+
Certificate[] certs = null;
90+
URL location = (grantEntry.codeBase() != null) ? newURL(grantEntry.codeBase()) : null;
91+
return canonicalizeCodebase(new CodeSource(location, certs));
92+
} catch (Exception e) {
93+
throw new PolicyInitializationException("Failed to get CodeSource", e);
94+
}
95+
}
96+
97+
private void addGrantEntry(GrantEntry grantEntry, PolicyInfo newInfo) throws PolicyInitializationException {
98+
CodeSource codesource = getCodeSource(grantEntry);
99+
if (codesource == null) {
100+
throw new PolicyInitializationException("Null CodeSource for: " + grantEntry.codeBase());
101+
}
102+
103+
List<Permission> permissions = new ArrayList<>();
104+
List<PermissionEntry> permissionList = grantEntry.permissionEntries();
105+
for (PermissionEntry pe : permissionList) {
106+
final PermissionEntry expandedEntry = expandPermissionName(pe);
107+
try {
108+
Optional<Permission> perm = getInstance(expandedEntry.permission(), expandedEntry.name(), expandedEntry.action());
109+
if (perm.isPresent()) {
110+
permissions.add(perm.get());
111+
}
112+
} catch (ClassNotFoundException e) {
113+
// these were mostly custom permission classes added for security
114+
// manager. Since security manager is deprecated, we can skip these
115+
// permissions classes.
116+
if (PERM_CLASSES_TO_SKIP.contains(pe.permission())) {
117+
continue; // skip this permission
118+
}
119+
throw new PolicyInitializationException("Permission class not found: " + pe.permission(), e);
120+
}
121+
}
122+
newInfo.policyEntries.add(new PolicyEntry(codesource, permissions));
123+
}
124+
125+
private static PermissionEntry expandPermissionName(PermissionEntry pe) {
126+
if (pe.name() == null || !pe.name().contains("${{")) {
127+
return pe;
128+
}
129+
130+
int startIndex = 0;
131+
int b, e;
132+
StringBuilder sb = new StringBuilder();
133+
134+
while ((b = pe.name().indexOf("${{", startIndex)) != -1 && (e = pe.name().indexOf("}}", b)) != -1) {
135+
sb.append(pe.name(), startIndex, b);
136+
String value = pe.name().substring(b + 3, e);
137+
sb.append("${{").append(value).append("}}");
138+
startIndex = e + 2;
139+
}
140+
141+
sb.append(pe.name().substring(startIndex));
142+
return new PermissionEntry(pe.permission(), sb.toString(), pe.action());
143+
}
144+
145+
private static final Optional<Permission> getInstance(String type, String name, String actions) throws ClassNotFoundException {
146+
Class<?> pc = Class.forName(type, false, null);
147+
Permission answer = getKnownPermission(pc, name, actions);
148+
149+
return Optional.ofNullable(answer);
150+
}
151+
152+
private static Permission getKnownPermission(Class<?> claz, String name, String actions) {
153+
if (claz.equals(FilePermission.class)) {
154+
return new FilePermission(name, actions);
155+
} else if (claz.equals(SocketPermission.class)) {
156+
return new SocketPermission(name, actions);
157+
} else if (claz.equals(RuntimePermission.class)) {
158+
return new RuntimePermission(name, actions);
159+
} else if (claz.equals(PropertyPermission.class)) {
160+
return new PropertyPermission(name, actions);
161+
} else if (claz.equals(NetPermission.class)) {
162+
return new NetPermission(name, actions);
163+
} else if (claz.equals(AllPermission.class)) {
164+
return new AllPermission();
165+
} else if (claz.equals(SecurityPermission.class)) {
166+
return new SecurityPermission(name, actions);
167+
} else {
168+
return null;
169+
}
170+
}
171+
172+
@Override
173+
public void refresh() {
174+
try {
175+
init(url);
176+
} catch (PolicyInitializationException e) {
177+
throw new RuntimeException("Failed to refresh policy", e);
178+
}
179+
}
180+
181+
@Override
182+
public boolean implies(ProtectionDomain pd, Permission p) {
183+
PermissionCollection pc = getPermissions(pd);
184+
return pc != null && pc.implies(p);
185+
}
186+
187+
@Override
188+
public PermissionCollection getPermissions(ProtectionDomain domain) {
189+
Permissions perms = new Permissions();
190+
if (domain == null) return perms;
191+
192+
try {
193+
getPermissionsForProtectionDomain(perms, domain);
194+
} catch (PolicyInitializationException e) {
195+
throw new RuntimeException("Failed to get permissions for domain", e);
196+
}
197+
198+
PermissionCollection pc = domain.getPermissions();
199+
if (pc != null) {
200+
synchronized (pc) {
201+
Enumeration<Permission> e = pc.elements();
202+
while (e.hasMoreElements()) {
203+
perms.add(e.nextElement());
204+
}
205+
}
206+
}
207+
208+
return perms;
209+
}
210+
211+
@Override
212+
public PermissionCollection getPermissions(CodeSource codesource) {
213+
if (codesource == null) return new Permissions();
214+
215+
Permissions perms = new Permissions();
216+
CodeSource canonicalCodeSource;
217+
218+
try {
219+
canonicalCodeSource = canonicalizeCodebase(codesource);
220+
} catch (PolicyInitializationException e) {
221+
throw new RuntimeException("Failed to canonicalize CodeSource", e);
222+
}
223+
224+
for (PolicyEntry entry : policyInfo.policyEntries) {
225+
if (entry.codeSource().implies(canonicalCodeSource)) {
226+
for (Permission permission : entry.permissions) {
227+
perms.add(permission);
228+
}
229+
}
230+
}
231+
232+
return perms;
233+
}
234+
235+
private void getPermissionsForProtectionDomain(Permissions perms, ProtectionDomain pd) throws PolicyInitializationException {
236+
final CodeSource cs = pd.getCodeSource();
237+
if (cs == null) return;
238+
239+
CodeSource canonicalCodeSource = canonicalizeCodebase(cs);
240+
241+
for (PolicyEntry entry : policyInfo.policyEntries) {
242+
if (entry.codeSource().implies(canonicalCodeSource)) {
243+
for (Permission permission : entry.permissions) {
244+
perms.add(permission);
245+
}
246+
}
247+
}
248+
}
249+
250+
private CodeSource canonicalizeCodebase(CodeSource cs) throws PolicyInitializationException {
251+
URL location = cs.getLocation();
252+
if (location == null) return cs;
253+
254+
try {
255+
URL canonicalUrl = canonicalizeUrl(location);
256+
return new CodeSource(canonicalUrl, cs.getCertificates());
257+
} catch (IOException e) {
258+
throw new PolicyInitializationException("Failed to canonicalize CodeSource", e);
259+
}
260+
}
261+
262+
@SuppressWarnings("deprecation")
263+
private URL canonicalizeUrl(URL url) throws IOException {
264+
String protocol = url.getProtocol();
265+
266+
if ("jar".equals(protocol)) {
267+
String spec = url.getFile();
268+
int separator = spec.indexOf("!/");
269+
if (separator != -1) {
270+
try {
271+
url = new URL(spec.substring(0, separator));
272+
} catch (MalformedURLException e) {
273+
throw new IOException("Malformed nested jar URL", e);
274+
}
275+
}
276+
}
277+
278+
if ("file".equals(url.getProtocol())) {
279+
String path = url.getPath();
280+
path = canonicalizePath(path);
281+
return new File(path).toURI().toURL();
282+
}
283+
284+
return url;
285+
}
286+
287+
private String canonicalizePath(String path) throws IOException {
288+
if (path.endsWith("*")) {
289+
path = path.substring(0, path.length() - 1);
290+
return new File(path).getCanonicalPath() + "*";
291+
} else {
292+
return new File(path).getCanonicalPath();
293+
}
294+
}
295+
296+
private record PolicyEntry(CodeSource codeSource, List<Permission> permissions) {
297+
@Override
298+
public String toString() {
299+
StringBuilder sb = new StringBuilder();
300+
sb.append("{").append(codeSource).append("\n");
301+
for (Permission p : permissions) {
302+
sb.append(" ").append(p).append("\n");
303+
}
304+
sb.append("}\n");
305+
return sb.toString();
306+
}
307+
}
308+
309+
private static class PolicyInfo {
310+
final List<PolicyEntry> policyEntries;
311+
312+
PolicyInfo() {
313+
policyEntries = new ArrayList<>();
314+
}
315+
}
316+
317+
private static URL newURL(String spec) throws MalformedURLException, URISyntaxException {
318+
return new URI(spec).toURL();
319+
}
320+
}

0 commit comments

Comments
 (0)