From 893b7b09706bc4267140d76aea36d0291ba2e4b4 Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Wed, 27 Nov 2024 13:53:28 +0200 Subject: [PATCH 1/8] [Security Manager Replacement] Native Java Agent (dynamic code rewriting, must be low overhead) Signed-off-by: Andriy Redko Signed-off-by: Andriy Redko --- build.gradle | 8 +- buildSrc/build.gradle | 4 +- .../gradle/OpenSearchTestBasePlugin.java | 3 +- .../gradle/test/DistroTestPlugin.java | 4 +- .../org/opensearch/bootstrap/test.policy | 1 + distribution/archives/build.gradle | 9 + distribution/build.gradle | 12 + distribution/src/config/jvm.options | 5 +- .../tools/launchers/SystemJvmOptions.java | 2 +- gradle/ide.gradle | 2 +- gradle/libs.versions.toml | 2 +- gradle/missing-javadoc.gradle | 1 + libs/agent-sm/agent-policy/build.gradle | 26 + .../java/org/opensearch/package-info.java | 12 + .../secure_sm/policy/ParseUtil.java | 616 +++++++ .../opensearch/secure_sm/policy/Password.java | 173 ++ .../secure_sm/policy/PolicyFile.java | 1601 +++++++++++++++++ .../secure_sm/policy/PolicyParser.java | 1163 ++++++++++++ .../secure_sm/policy/PolicyUtil.java | 170 ++ .../secure_sm/policy/PropertyExpander.java | 133 ++ .../secure_sm/policy/SecurityConstants.java | 145 ++ .../secure_sm/policy/package-info.java | 12 + libs/agent-sm/agent/build.gradle | 4 + .../javaagent/SystemExitInterceptor.java | 6 + libs/build.gradle | 25 +- .../nio/SocketChannelContextTests.java | 2 + libs/secure-sm/build.gradle | 1 + plugins/repository-hdfs/build.gradle | 2 +- .../org/opensearch/bootstrap/test.policy | 12 + .../org/opensearch/bootstrap/test.policy | 12 + server/build.gradle | 3 +- .../opensearch/bootstrap/BootstrapChecks.java | 8 +- .../org/opensearch/bootstrap/OpenSearch.java | 13 - .../org/opensearch/bootstrap/Security.java | 28 +- .../common/util/concurrent/ThreadContext.java | 19 - .../org/opensearch/bootstrap/security.policy | 24 + .../bootstrap/test-framework.policy | 10 +- .../ExceptionSerializationTests.java | 6 +- .../org/opensearch/bootstrap/test.policy | 6 + test/framework/build.gradle | 8 +- .../org/opensearch/bootstrap/AgentAttach.java | 20 + .../bootstrap/BootstrapForTesting.java | 11 +- .../org/opensearch/bootstrap/test.policy | 16 + 43 files changed, 4260 insertions(+), 80 deletions(-) create mode 100644 libs/agent-sm/agent-policy/build.gradle create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/package-info.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Password.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/package-info.java create mode 100644 plugins/repository-hdfs/src/test/resources/org/opensearch/bootstrap/test.policy create mode 100644 plugins/repository-s3/src/internalClusterTest/resources/org/opensearch/bootstrap/test.policy create mode 100644 test/framework/src/main/java/org/opensearch/bootstrap/AgentAttach.java create mode 100644 test/framework/src/test/resources/org/opensearch/bootstrap/test.policy diff --git a/build.gradle b/build.gradle index 187574da9e62a..579508137c291 100644 --- a/build.gradle +++ b/build.gradle @@ -433,12 +433,18 @@ gradle.projectsEvaluated { project.tasks.withType(Test) { task -> if (task != null) { - if (BuildParams.runtimeJavaVersion > JavaVersion.VERSION_17) { + if (BuildParams.runtimeJavaVersion > JavaVersion.VERSION_17 && BuildParams.runtimeJavaVersion <= JavaVersion.VERSION_23) { task.jvmArgs += ["-Djava.security.manager=allow"] } if (BuildParams.runtimeJavaVersion >= JavaVersion.VERSION_20) { task.jvmArgs += ["--add-modules=jdk.incubator.vector"] } + + // Add Java Agent for security sandboxing + if (!(project.path in [':build-tools', ":libs:agent-sm:bootstrap", ":libs:agent-sm:agent"])) { + dependsOn(project(':libs:agent-sm:agent').prepareAgent) + jvmArgs += ["-javaagent:" + project(':libs:agent-sm:agent').jar.archiveFile.get()] + } } } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 65986f2361c9d..e8459443e8a04 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -110,12 +110,12 @@ dependencies { api 'com.netflix.nebula:gradle-info-plugin:12.1.6' api 'org.apache.rat:apache-rat:0.15' api "commons-io:commons-io:${props.getProperty('commonsio')}" - api "net.java.dev.jna:jna:5.14.0" + api "net.java.dev.jna:jna:5.16.0" api 'com.gradleup.shadow:shadow-gradle-plugin:8.3.5' api 'org.jdom:jdom2:2.0.6.1' api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${props.getProperty('kotlin')}" api 'de.thetaphi:forbiddenapis:3.8' - api 'com.avast.gradle:gradle-docker-compose-plugin:0.17.6' + api 'com.avast.gradle:gradle-docker-compose-plugin:0.17.12' api "org.yaml:snakeyaml:${props.getProperty('snakeyaml')}" api 'org.apache.maven:maven-model:3.9.6' api 'com.networknt:json-schema-validator:1.2.0' diff --git a/buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java b/buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java index d79dfb1124757..4932009132457 100644 --- a/buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java +++ b/buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java @@ -115,7 +115,8 @@ public void execute(Task t) { test.jvmArgs("--illegal-access=warn"); } } - if (test.getJavaVersion().compareTo(JavaVersion.VERSION_17) > 0) { + if (test.getJavaVersion().compareTo(JavaVersion.VERSION_17) > 0 + && test.getJavaVersion().compareTo(JavaVersion.VERSION_24) < 0) { test.jvmArgs("-Djava.security.manager=allow"); } } diff --git a/buildSrc/src/main/java/org/opensearch/gradle/test/DistroTestPlugin.java b/buildSrc/src/main/java/org/opensearch/gradle/test/DistroTestPlugin.java index 888cd8d4bf5b5..654af7da65662 100644 --- a/buildSrc/src/main/java/org/opensearch/gradle/test/DistroTestPlugin.java +++ b/buildSrc/src/main/java/org/opensearch/gradle/test/DistroTestPlugin.java @@ -77,9 +77,9 @@ import java.util.stream.Stream; public class DistroTestPlugin implements Plugin { - private static final String SYSTEM_JDK_VERSION = "21.0.6+7"; + private static final String SYSTEM_JDK_VERSION = "23.0.2+7"; private static final String SYSTEM_JDK_VENDOR = "adoptium"; - private static final String GRADLE_JDK_VERSION = "21.0.6+7"; + private static final String GRADLE_JDK_VERSION = "23.0.2+7"; private static final String GRADLE_JDK_VENDOR = "adoptium"; // all distributions used by distro tests. this is temporary until tests are per distribution diff --git a/client/rest-high-level/src/test/resources/org/opensearch/bootstrap/test.policy b/client/rest-high-level/src/test/resources/org/opensearch/bootstrap/test.policy index 2604c2492d8ab..96cd3e9f148cf 100644 --- a/client/rest-high-level/src/test/resources/org/opensearch/bootstrap/test.policy +++ b/client/rest-high-level/src/test/resources/org/opensearch/bootstrap/test.policy @@ -8,4 +8,5 @@ grant { permission java.net.SocketPermission "*", "connect,resolve"; + permission java.net.NetPermission "accessUnixDomainSocket"; }; diff --git a/distribution/archives/build.gradle b/distribution/archives/build.gradle index 792b1ab57ddbc..f42dc422cb938 100644 --- a/distribution/archives/build.gradle +++ b/distribution/archives/build.gradle @@ -38,6 +38,9 @@ CopySpec archiveFiles(CopySpec modulesFiles, String distributionType, String pla into('lib') { with libFiles() } + into('agent') { + with agentFiles() + } into('config') { dirPermissions { unix 0750 @@ -226,3 +229,9 @@ subprojects { group = "org.opensearch.distribution" } + +tasks.each { + if (it.name.startsWith("build")) { + it.dependsOn project(':libs:agent-sm:agent').assemble + } +} diff --git a/distribution/build.gradle b/distribution/build.gradle index 8fe9a89059a50..e863d5ab21fe0 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -357,6 +357,18 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { } } + agentFiles = { + copySpec { + from(project(':libs:agent-sm:agent').prepareAgent) { + include '**/*.jar' + exclude '**/*-javadoc.jar' + exclude '**/*-sources.jar' + // strip the version since jvm.options is using agent without version + rename("opensearch-agent-${project.version}.jar", "opensearch-agent.jar") + } + } + } + modulesFiles = { platform -> copySpec { eachFile { diff --git a/distribution/src/config/jvm.options b/distribution/src/config/jvm.options index a8c96f33ce51d..2e3d8474b9a3b 100644 --- a/distribution/src/config/jvm.options +++ b/distribution/src/config/jvm.options @@ -77,7 +77,7 @@ ${error.file} 9-:-Xlog:gc*,gc+age=trace,safepoint:file=${loggc}:utctime,pid,tags:filecount=32,filesize=64m # Explicitly allow security manager (https://bugs.openjdk.java.net/browse/JDK-8270380) -18-:-Djava.security.manager=allow +18-23:-Djava.security.manager=allow # JDK 20+ Incubating Vector Module for SIMD optimizations; # disabling may reduce performance on vector optimized lucene @@ -89,3 +89,6 @@ ${error.file} # See please https://bugs.openjdk.org/browse/JDK-8341127 (openjdk/jdk#21283) 23:-XX:CompileCommand=dontinline,java/lang/invoke/MethodHandle.setAsTypeCache 23:-XX:CompileCommand=dontinline,java/lang/invoke/MethodHandle.asTypeUncached + +# It should be JDK-24 (but we cannot bring JDK-24 since Gradle does not support it yet) +21-:-javaagent:agent/opensearch-agent.jar diff --git a/distribution/tools/launchers/src/main/java/org/opensearch/tools/launchers/SystemJvmOptions.java b/distribution/tools/launchers/src/main/java/org/opensearch/tools/launchers/SystemJvmOptions.java index af7138569972a..5bedb3ac5ca3e 100644 --- a/distribution/tools/launchers/src/main/java/org/opensearch/tools/launchers/SystemJvmOptions.java +++ b/distribution/tools/launchers/src/main/java/org/opensearch/tools/launchers/SystemJvmOptions.java @@ -85,7 +85,7 @@ static List systemJvmOptions() { } private static String allowSecurityManagerOption() { - if (Runtime.version().feature() > 17) { + if (Runtime.version().feature() > 17 && Runtime.version().feature() < 24) { return "-Djava.security.manager=allow"; } else { return ""; diff --git a/gradle/ide.gradle b/gradle/ide.gradle index c16205468d63d..79df92abec2e5 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -82,7 +82,7 @@ if (System.getProperty('idea.active') == 'true') { runConfigurations { defaults(JUnit) { vmParameters = '-ea -Djava.locale.providers=SPI,CLDR' - if (BuildParams.runtimeJavaVersion > JavaVersion.VERSION_17) { + if (BuildParams.runtimeJavaVersion > JavaVersion.VERSION_17 && BuildParams.runtimeJavaVersion < JavaVersion.VERSION_24) { vmParameters += ' -Djava.security.manager=allow' } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d3aebf83eecc..0995bdafcf7b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ opensearch = "3.0.0" lucene = "10.1.0" bundled_jdk_vendor = "adoptium" -bundled_jdk = "21.0.6+7" +bundled_jdk = "23.0.2+7" # optional dependencies spatial4j = "0.7" diff --git a/gradle/missing-javadoc.gradle b/gradle/missing-javadoc.gradle index 6e31f838e678a..9f27dc5cadcd2 100644 --- a/gradle/missing-javadoc.gradle +++ b/gradle/missing-javadoc.gradle @@ -106,6 +106,7 @@ configure([ project(":libs:opensearch-secure-sm"), project(":libs:opensearch-ssl-config"), project(":libs:opensearch-x-content"), + project(":libs:agent-sm:agent-policy"), project(":modules:aggs-matrix-stats"), project(":modules:analysis-common"), project(":modules:geo"), diff --git a/libs/agent-sm/agent-policy/build.gradle b/libs/agent-sm/agent-policy/build.gradle new file mode 100644 index 0000000000000..997ed5ddf174b --- /dev/null +++ b/libs/agent-sm/agent-policy/build.gradle @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +apply plugin: 'opensearch.build' +apply plugin: 'opensearch.publish' + +ext { + failOnJavadocWarning = false +} + +base { + archivesName = 'opensearch-agent-policy' +} + +disableTasks('forbiddenApisMain') + +test.enabled = false +testingConventions.enabled = false diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/package-info.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/package-info.java new file mode 100644 index 0000000000000..0724b60d1777f --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * Java Agent Policy + */ +package org.opensearch; diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java new file mode 100644 index 0000000000000..d4477fa13fdcd --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java @@ -0,0 +1,616 @@ +/* + * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; + +/** + * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/net/www/ParseUtil.java + */ +public final class ParseUtil { + + private static final HexFormat HEX_UPPERCASE = HexFormat.of().withUpperCase(); + + private ParseUtil() {} + + /** + * Constructs an encoded version of the specified path string suitable + * for use in the construction of a URL. + * + * A path separator is replaced by a forward slash. The string is UTF8 + * encoded. The % escape sequence is used for characters that are above + * 0x7F or those defined in RFC2396 as reserved or excluded in the path + * component of a URL. + */ + public static String encodePath(String path) { + return encodePath(path, true); + } + + /* + * flag indicates whether path uses platform dependent + * File.separatorChar or not. True indicates path uses platform + * dependent File.separatorChar. + */ + public static String encodePath(String path, boolean flag) { + if (flag && File.separatorChar != '/') { + return encodePath(path, 0, File.separatorChar); + } else { + int index = firstEncodeIndex(path); + if (index > -1) { + return encodePath(path, index, '/'); + } else { + return path; + } + } + } + + private static int firstEncodeIndex(String path) { + int len = path.length(); + for (int i = 0; i < len; i++) { + char c = path.charAt(i); + // Ordering in the following test is performance sensitive, + // and typically paths have most chars in the a-z range, then + // in the symbol range '&'-':' (includes '.', '/' and '0'-'9') + // and more rarely in the A-Z range. + if (c >= 'a' && c <= 'z' || c >= '&' && c <= ':' || c >= 'A' && c <= 'Z') { + continue; + } else if (c > 0x007F || match(c, L_ENCODED, H_ENCODED)) { + return i; + } + } + return -1; + } + + private static String encodePath(String path, int index, char sep) { + char[] pathCC = path.toCharArray(); + char[] retCC = new char[pathCC.length * 2 + 16 - index]; + if (index > 0) { + System.arraycopy(pathCC, 0, retCC, 0, index); + } + int retLen = index; + + for (int i = index; i < pathCC.length; i++) { + char c = pathCC[i]; + if (c == sep) retCC[retLen++] = '/'; + else { + if (c <= 0x007F) { + if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9') { + retCC[retLen++] = c; + } else if (match(c, L_ENCODED, H_ENCODED)) { + retLen = escape(retCC, c, retLen); + } else { + retCC[retLen++] = c; + } + } else if (c > 0x07FF) { + retLen = escape(retCC, (char) (0xE0 | ((c >> 12) & 0x0F)), retLen); + retLen = escape(retCC, (char) (0x80 | ((c >> 6) & 0x3F)), retLen); + retLen = escape(retCC, (char) (0x80 | ((c >> 0) & 0x3F)), retLen); + } else { + retLen = escape(retCC, (char) (0xC0 | ((c >> 6) & 0x1F)), retLen); + retLen = escape(retCC, (char) (0x80 | ((c >> 0) & 0x3F)), retLen); + } + } + // worst case scenario for character [0x7ff-] every single + // character will be encoded into 9 characters. + if (retLen + 9 > retCC.length) { + int newLen = retCC.length * 2 + 16; + if (newLen < 0) { + newLen = Integer.MAX_VALUE; + } + char[] buf = new char[newLen]; + System.arraycopy(retCC, 0, buf, 0, retLen); + retCC = buf; + } + } + return new String(retCC, 0, retLen); + } + + /** + * Appends the URL escape sequence for the specified char to the + * specified character array. + */ + private static int escape(char[] cc, char c, int index) { + cc[index++] = '%'; + cc[index++] = Character.forDigit((c >> 4) & 0xF, 16); + cc[index++] = Character.forDigit(c & 0xF, 16); + return index; + } + + /** + * Un-escape and return the character at position i in string s. + */ + private static byte unescape(String s, int i) { + return (byte) Integer.parseInt(s, i + 1, i + 3, 16); + } + + /** + * Returns a new String constructed from the specified String by replacing + * the URL escape sequences and UTF8 encoding with the characters they + * represent. + */ + public static String decode(String s) { + int n = s.length(); + if ((n == 0) || (s.indexOf('%') < 0)) return s; + + StringBuilder sb = new StringBuilder(n); + ByteBuffer bb = ByteBuffer.allocate(n); + CharBuffer cb = CharBuffer.allocate(n); + CharsetDecoder dec = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + + char c = s.charAt(0); + for (int i = 0; i < n;) { + assert c == s.charAt(i); + if (c != '%') { + sb.append(c); + if (++i >= n) break; + c = s.charAt(i); + continue; + } + bb.clear(); + for (;;) { + if (n - i < 2) { + throw new IllegalArgumentException("Malformed escape pair: " + s); + } + + try { + bb.put(unescape(s, i)); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + throw new IllegalArgumentException("Malformed escape pair: " + s); + } + i += 3; + if (i >= n) break; + c = s.charAt(i); + if (c != '%') break; + } + bb.flip(); + cb.clear(); + dec.reset(); + CoderResult cr = dec.decode(bb, cb, true); + if (cr.isError()) throw new IllegalArgumentException("Error decoding percent encoded characters"); + cr = dec.flush(cb); + if (cr.isError()) throw new IllegalArgumentException("Error decoding percent encoded characters"); + sb.append(cb.flip().toString()); + } + + return sb.toString(); + } + + public static URL fileToEncodedURL(File file) throws MalformedURLException { + String path = file.getAbsolutePath(); + path = ParseUtil.encodePath(path); + if (!path.startsWith("/")) { + path = "/" + path; + } + if (!path.endsWith("/") && file.isDirectory()) { + path = path + "/"; + } + @SuppressWarnings("deprecation") + var result = new URL("file", "", path); + return result; + } + + public static java.net.URI toURI(URL url) { + String protocol = url.getProtocol(); + String auth = url.getAuthority(); + String path = url.getPath(); + String query = url.getQuery(); + String ref = url.getRef(); + if (path != null && !(path.startsWith("/"))) path = "/" + path; + + // + // In java.net.URI class, a port number of -1 implies the default + // port number. So get it stripped off before creating URI instance. + // + if (auth != null && auth.endsWith(":-1")) auth = auth.substring(0, auth.length() - 3); + + java.net.URI uri; + try { + uri = createURI(protocol, auth, path, query, ref); + } catch (java.net.URISyntaxException e) { + uri = null; + } + return uri; + } + + // + // createURI() and its auxiliary code are cloned from java.net.URI. + // Most of the code are just copy and paste, except that quote() + // has been modified to avoid double-escape. + // + // Usually it is unacceptable, but we're forced to do it because + // otherwise we need to change public API, namely java.net.URI's + // multi-argument constructors. It turns out that the changes cause + // incompatibilities so can't be done. + // + private static URI createURI(String scheme, String authority, String path, String query, String fragment) throws URISyntaxException { + String s = toString(scheme, null, authority, null, null, -1, path, query, fragment); + checkPath(s, scheme, path); + return new URI(s); + } + + private static String toString( + String scheme, + String opaquePart, + String authority, + String userInfo, + String host, + int port, + String path, + String query, + String fragment + ) { + StringBuilder sb = new StringBuilder(); + if (scheme != null) { + sb.append(scheme); + sb.append(':'); + } + appendSchemeSpecificPart(sb, opaquePart, authority, userInfo, host, port, path, query); + appendFragment(sb, fragment); + return sb.toString(); + } + + private static void appendSchemeSpecificPart( + StringBuilder sb, + String opaquePart, + String authority, + String userInfo, + String host, + int port, + String path, + String query + ) { + if (opaquePart != null) { + /* check if SSP begins with an IPv6 address + * because we must not quote a literal IPv6 address + */ + if (opaquePart.startsWith("//[")) { + int end = opaquePart.indexOf(']'); + if (end != -1 && opaquePart.indexOf(':') != -1) { + String doquote, dontquote; + if (end == opaquePart.length()) { + dontquote = opaquePart; + doquote = ""; + } else { + dontquote = opaquePart.substring(0, end + 1); + doquote = opaquePart.substring(end + 1); + } + sb.append(dontquote); + sb.append(quote(doquote, L_URIC, H_URIC)); + } + } else { + sb.append(quote(opaquePart, L_URIC, H_URIC)); + } + } else { + appendAuthority(sb, authority, userInfo, host, port); + if (path != null) sb.append(quote(path, L_PATH, H_PATH)); + if (query != null) { + sb.append('?'); + sb.append(quote(query, L_URIC, H_URIC)); + } + } + } + + private static void appendAuthority(StringBuilder sb, String authority, String userInfo, String host, int port) { + if (host != null) { + sb.append("//"); + if (userInfo != null) { + sb.append(quote(userInfo, L_USERINFO, H_USERINFO)); + sb.append('@'); + } + boolean needBrackets = ((host.indexOf(':') >= 0) && !host.startsWith("[") && !host.endsWith("]")); + if (needBrackets) sb.append('['); + sb.append(host); + if (needBrackets) sb.append(']'); + if (port != -1) { + sb.append(':'); + sb.append(port); + } + } else if (authority != null) { + sb.append("//"); + if (authority.startsWith("[")) { + int end = authority.indexOf(']'); + if (end != -1 && authority.indexOf(':') != -1) { + String doquote, dontquote; + if (end == authority.length()) { + dontquote = authority; + doquote = ""; + } else { + dontquote = authority.substring(0, end + 1); + doquote = authority.substring(end + 1); + } + sb.append(dontquote); + sb.append(quote(doquote, L_REG_NAME | L_SERVER, H_REG_NAME | H_SERVER)); + } + } else { + sb.append(quote(authority, L_REG_NAME | L_SERVER, H_REG_NAME | H_SERVER)); + } + } + } + + private static void appendFragment(StringBuilder sb, String fragment) { + if (fragment != null) { + sb.append('#'); + sb.append(quote(fragment, L_URIC, H_URIC)); + } + } + + // Quote any characters in s that are not permitted + // by the given mask pair + // + private static String quote(String s, long lowMask, long highMask) { + int n = s.length(); + StringBuilder sb = null; + CharsetEncoder encoder = null; + boolean allowNonASCII = ((lowMask & L_ESCAPED) != 0); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c < '\u0080') { + if (!match(c, lowMask, highMask) && !isEscaped(s, i)) { + if (sb == null) { + sb = new StringBuilder(); + sb.append(s, 0, i); + } + appendEscape(sb, (byte) c); + } else { + if (sb != null) sb.append(c); + } + } else if (allowNonASCII && (Character.isSpaceChar(c) || Character.isISOControl(c))) { + if (encoder == null) { + encoder = StandardCharsets.UTF_8.newEncoder(); + } + if (sb == null) { + sb = new StringBuilder(); + sb.append(s, 0, i); + } + appendEncoded(encoder, sb, c); + } else { + if (sb != null) sb.append(c); + } + } + return (sb == null) ? s : sb.toString(); + } + + // + // To check if the given string has an escaped triplet + // at the given position + // + private static boolean isEscaped(String s, int pos) { + if (s == null || (s.length() <= (pos + 2))) return false; + + return s.charAt(pos) == '%' && match(s.charAt(pos + 1), L_HEX, H_HEX) && match(s.charAt(pos + 2), L_HEX, H_HEX); + } + + private static void appendEncoded(CharsetEncoder encoder, StringBuilder sb, char c) { + ByteBuffer bb = null; + try { + bb = encoder.encode(CharBuffer.wrap("" + c)); + } catch (CharacterCodingException x) { + assert false; + } + while (bb.hasRemaining()) { + int b = bb.get() & 0xff; + if (b >= 0x80) appendEscape(sb, (byte) b); + else sb.append((char) b); + } + } + + private static void appendEscape(StringBuilder sb, byte b) { + sb.append('%'); + HEX_UPPERCASE.toHexDigits(sb, b); + } + + // Tell whether the given character is permitted by the given mask pair + private static boolean match(char c, long lowMask, long highMask) { + if (c < 64) return ((1L << c) & lowMask) != 0; + if (c < 128) return ((1L << (c - 64)) & highMask) != 0; + return false; + } + + // If a scheme is given then the path, if given, must be absolute + // + private static void checkPath(String s, String scheme, String path) throws URISyntaxException { + if (scheme != null) { + if (path != null && !path.isEmpty() && path.charAt(0) != '/') throw new URISyntaxException(s, "Relative path in absolute URI"); + } + } + + // -- Character classes for parsing -- + + // To save startup time, we manually calculate the low-/highMask constants. + // For reference, the following methods were used to calculate the values: + + // Compute a low-order mask for the characters + // between first and last, inclusive + // private static long lowMask(char first, char last) { + // long m = 0; + // int f = Math.max(Math.min(first, 63), 0); + // int l = Math.max(Math.min(last, 63), 0); + // for (int i = f; i <= l; i++) + // m |= 1L << i; + // return m; + // } + + // Compute the low-order mask for the characters in the given string + // private static long lowMask(String chars) { + // int n = chars.length(); + // long m = 0; + // for (int i = 0; i < n; i++) { + // char c = chars.charAt(i); + // if (c < 64) + // m |= (1L << c); + // } + // return m; + // } + + // Compute a high-order mask for the characters + // between first and last, inclusive + // private static long highMask(char first, char last) { + // long m = 0; + // int f = Math.max(Math.min(first, 127), 64) - 64; + // int l = Math.max(Math.min(last, 127), 64) - 64; + // for (int i = f; i <= l; i++) + // m |= 1L << i; + // return m; + // } + + // Compute the high-order mask for the characters in the given string + // private static long highMask(String chars) { + // int n = chars.length(); + // long m = 0; + // for (int i = 0; i < n; i++) { + // char c = chars.charAt(i); + // if ((c >= 64) && (c < 128)) + // m |= (1L << (c - 64)); + // } + // return m; + // } + + // Character-class masks + + // digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | + // "8" | "9" + private static final long L_DIGIT = 0x3FF000000000000L; // lowMask('0', '9'); + private static final long H_DIGIT = 0L; + + // hex = digit | "A" | "B" | "C" | "D" | "E" | "F" | + // "a" | "b" | "c" | "d" | "e" | "f" + private static final long L_HEX = L_DIGIT; + private static final long H_HEX = 0x7E0000007EL; // highMask('A', 'F') | highMask('a', 'f'); + + // upalpha = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | + // "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | + // "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" + private static final long L_UPALPHA = 0L; + private static final long H_UPALPHA = 0x7FFFFFEL; // highMask('A', 'Z'); + + // lowalpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | + // "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | + // "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" + private static final long L_LOWALPHA = 0L; + private static final long H_LOWALPHA = 0x7FFFFFE00000000L; // highMask('a', 'z'); + + // alpha = lowalpha | upalpha + private static final long L_ALPHA = L_LOWALPHA | L_UPALPHA; + private static final long H_ALPHA = H_LOWALPHA | H_UPALPHA; + + // alphanum = alpha | digit + private static final long L_ALPHANUM = L_DIGIT | L_ALPHA; + private static final long H_ALPHANUM = H_DIGIT | H_ALPHA; + + // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | + // "(" | ")" + private static final long L_MARK = 0x678200000000L; // lowMask("-_.!~*'()"); + private static final long H_MARK = 0x4000000080000000L; // highMask("-_.!~*'()"); + + // unreserved = alphanum | mark + private static final long L_UNRESERVED = L_ALPHANUM | L_MARK; + private static final long H_UNRESERVED = H_ALPHANUM | H_MARK; + + // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + // "$" | "," | "[" | "]" + // Added per RFC2732: "[", "]" + private static final long L_RESERVED = 0xAC00985000000000L; // lowMask(";/?:@&=+$,[]"); + private static final long H_RESERVED = 0x28000001L; // highMask(";/?:@&=+$,[]"); + + // The zero'th bit is used to indicate that escape pairs and non-US-ASCII + // characters are allowed; this is handled by the scanEscape method below. + private static final long L_ESCAPED = 1L; + private static final long H_ESCAPED = 0L; + + // uric = reserved | unreserved | escaped + private static final long L_URIC = L_RESERVED | L_UNRESERVED | L_ESCAPED; + private static final long H_URIC = H_RESERVED | H_UNRESERVED | H_ESCAPED; + + // pchar = unreserved | escaped | + // ":" | "@" | "&" | "=" | "+" | "$" | "," + private static final long L_PCHAR = L_UNRESERVED | L_ESCAPED | 0x2400185000000000L; // lowMask(":@&=+$,"); + private static final long H_PCHAR = H_UNRESERVED | H_ESCAPED | 0x1L; // highMask(":@&=+$,"); + + // All valid path characters + private static final long L_PATH = L_PCHAR | 0x800800000000000L; // lowMask(";/"); + private static final long H_PATH = H_PCHAR; // highMask(";/") == 0x0L; + + // Dash, for use in domainlabel and toplabel + private static final long L_DASH = 0x200000000000L; // lowMask("-"); + private static final long H_DASH = 0x0L; // highMask("-"); + + // userinfo = *( unreserved | escaped | + // ";" | ":" | "&" | "=" | "+" | "$" | "," ) + private static final long L_USERINFO = L_UNRESERVED | L_ESCAPED | 0x2C00185000000000L; // lowMask(";:&=+$,"); + private static final long H_USERINFO = H_UNRESERVED | H_ESCAPED; // | highMask(";:&=+$,") == 0L; + + // reg_name = 1*( unreserved | escaped | "$" | "," | + // ";" | ":" | "@" | "&" | "=" | "+" ) + private static final long L_REG_NAME = L_UNRESERVED | L_ESCAPED | 0x2C00185000000000L; // lowMask("$,;:@&=+"); + private static final long H_REG_NAME = H_UNRESERVED | H_ESCAPED | 0x1L; // highMask("$,;:@&=+"); + + // All valid characters for server-based authorities + private static final long L_SERVER = L_USERINFO | L_ALPHANUM | L_DASH | 0x400400000000000L; // lowMask(".:@[]"); + private static final long H_SERVER = H_USERINFO | H_ALPHANUM | H_DASH | 0x28000001L; // highMask(".:@[]"); + + // Characters that are encoded in the path component of a URI. + // + // These characters are reserved in the path segment as described in + // RFC2396 section 3.3: + // "=" | ";" | "?" | "/" + // + // These characters are defined as excluded in RFC2396 section 2.4.3 + // and must be escaped if they occur in the data part of a URI: + // "#" | " " | "<" | ">" | "%" | "\"" | "{" | "}" | "|" | "\\" | "^" | + // "[" | "]" | "`" + // + // Also US ASCII control characters 00-1F and 7F. + + // lowMask((char)0, (char)31) | lowMask("=;?/# <>%\"{}|\\^[]`"); + private static final long L_ENCODED = 0xF800802DFFFFFFFFL; + + // highMask((char)0x7F, (char)0x7F) | highMask("=;?/# <>%\"{}|\\^[]`"); + private static final long H_ENCODED = 0xB800000178000000L; + +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Password.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Password.java new file mode 100644 index 0000000000000..ffe5f734fa0ea --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Password.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +import java.io.ByteArrayInputStream; +import java.io.Console; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; +import java.util.Arrays; + +/** + * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/util/Password.java + */ +public class Password { + /** Reads user password from given input stream. */ + public static char[] readPassword(InputStream in) throws IOException { + return readPassword(in, false); + } + + /** Reads user password from given input stream. + * @param isEchoOn true if the password should be echoed on the screen + */ + @SuppressWarnings("fallthrough") + public static char[] readPassword(InputStream in, boolean isEchoOn) throws IOException { + + char[] consoleEntered = null; + byte[] consoleBytes = null; + + try { + // Use the new java.io.Console class + Console con = null; + if (!isEchoOn && in == System.in && ((con = System.console()) != null)) { + consoleEntered = con.readPassword(); + // readPassword returns "" if you just print ENTER, + // to be compatible with old Password class, change to null + if (consoleEntered != null && consoleEntered.length == 0) { + return null; + } + consoleBytes = convertToBytes(consoleEntered); + in = new ByteArrayInputStream(consoleBytes); + } + + // Rest of the lines still necessary for KeyStoreLoginModule + // and when there is no console. + + char[] lineBuffer; + char[] buf; + int i; + + buf = lineBuffer = new char[128]; + + int room = buf.length; + int offset = 0; + int c; + + boolean done = false; + while (!done) { + switch (c = in.read()) { + case -1: + case '\n': + done = true; + break; + + case '\r': + int c2 = in.read(); + if ((c2 != '\n') && (c2 != -1)) { + if (!(in instanceof PushbackInputStream)) { + in = new PushbackInputStream(in); + } + ((PushbackInputStream) in).unread(c2); + } else { + done = true; + break; + } + /* fall through */ + default: + if (--room < 0) { + buf = new char[offset + 128]; + room = buf.length - offset - 1; + System.arraycopy(lineBuffer, 0, buf, 0, offset); + Arrays.fill(lineBuffer, ' '); + lineBuffer = buf; + } + buf[offset++] = (char) c; + break; + } + } + + if (offset == 0) { + return null; + } + + char[] ret = new char[offset]; + System.arraycopy(buf, 0, ret, 0, offset); + Arrays.fill(buf, ' '); + + return ret; + } finally { + if (consoleEntered != null) { + Arrays.fill(consoleEntered, ' '); + } + if (consoleBytes != null) { + Arrays.fill(consoleBytes, (byte) 0); + } + } + } + + /** + * Change a password read from Console.readPassword() into + * its original bytes. + * + * @param pass a char[] + * @return its byte[] format, similar to new String(pass).getBytes() + */ + private static byte[] convertToBytes(char[] pass) { + if (enc == null) { + synchronized (Password.class) { + enc = System.console() + .charset() + .newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + } + } + byte[] ba = new byte[(int) (enc.maxBytesPerChar() * pass.length)]; + ByteBuffer bb = ByteBuffer.wrap(ba); + synchronized (enc) { + enc.reset().encode(CharBuffer.wrap(pass), bb, true); + } + if (bb.position() < ba.length) { + ba[bb.position()] = '\n'; + } + return ba; + } + + private static volatile CharsetEncoder enc; +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java new file mode 100644 index 0000000000000..14b1a8f56375c --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java @@ -0,0 +1,1601 @@ +/* + * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +import javax.security.auth.Subject; +import javax.security.auth.x500.X500Principal; + +import java.io.File; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.NetPermission; +import java.net.SocketPermission; +import java.net.URI; +import java.net.URL; +import java.security.AllPermission; +import java.security.CodeSource; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Permissions; +import java.security.Principal; +import java.security.ProtectionDomain; +import java.security.Security; +import java.security.SecurityPermission; +import java.security.UnresolvedPermission; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.PropertyPermission; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/provider/PolicyFile.java + */ +@SuppressWarnings("removal") +public class PolicyFile extends java.security.Policy { + private static final String SELF = "${{self}}"; + private static final String X500PRINCIPAL = "javax.security.auth.x500.X500Principal"; + private static final String POLICY = "java.security.policy"; + private static final String POLICY_URL = "policy.url."; + + private static final int DEFAULT_CACHE_SIZE = 1; + + // contains the policy grant entries, PD cache, and alias mapping + // can be updated if refresh() is called + private volatile PolicyInfo policyInfo; + + private boolean expandProperties = true; + private boolean allowSystemProperties = true; + private boolean notUtf8 = false; + private URL url; + + // for use with the reflection API + private static final Class[] PARAMS0 = {}; + private static final Class[] PARAMS1 = { String.class }; + private static final Class[] PARAMS2 = { String.class, String.class }; + + /** + * When a policy file has a syntax error, the exception code may generate + * another permission check and this can cause the policy file to be parsed + * repeatedly, leading to a StackOverflowError or ClassCircularityError. + * To avoid this, this set is populated with policy files that have been + * previously parsed and have syntax errors, so that they can be + * subsequently ignored. + */ + private static Set badPolicyURLs = Collections.newSetFromMap(new ConcurrentHashMap()); + + /** + * Initializes the Policy object and reads the default policy + * configuration file(s) into the Policy object. + */ + public PolicyFile() { + init((URL) null); + } + + /** + * Initializes the Policy object and reads the default policy + * from the specified URL only. + */ + public PolicyFile(URL url) { + this.url = url; + init(url); + } + + /** + * Initializes the Policy object and reads the default policy + * configuration file(s) into the Policy object. + * + * See the class description for details on the algorithm used to + * initialize the Policy object. + */ + private void init(URL url) { + int numCaches = DEFAULT_CACHE_SIZE; + PolicyInfo newInfo = new PolicyInfo(numCaches); + initPolicyFile(newInfo, url); + policyInfo = newInfo; + } + + private void initPolicyFile(final PolicyInfo newInfo, final URL url) { + if (url != null) { + + /** + * If the caller specified a URL via Policy.getInstance, + * we only read from default.policy and that URL. + */ + + if (init(url, newInfo) == false) { + // use static policy if all else fails + initStaticPolicy(newInfo); + } + + } else { + + /** + * Caller did not specify URL via Policy.getInstance. + * Read from URLs listed in the java.security properties file. + */ + + boolean loaded_one = initPolicyFile(POLICY, POLICY_URL, newInfo); + // To maintain strict backward compatibility + // we load the static policy only if POLICY load failed + if (!loaded_one) { + // use static policy if all else fails + initStaticPolicy(newInfo); + } + } + } + + private boolean initPolicyFile(final String propname, final String urlname, final PolicyInfo newInfo) { + boolean loaded_policy = false; + + if (allowSystemProperties) { + String extra_policy = System.getProperty(propname); + if (extra_policy != null) { + boolean overrideAll = false; + if (extra_policy.startsWith("=")) { + overrideAll = true; + extra_policy = extra_policy.substring(1); + } + try { + extra_policy = PropertyExpander.expand(extra_policy); + URL policyURL; + + File policyFile = new File(extra_policy); + if (policyFile.exists()) { + policyURL = ParseUtil.fileToEncodedURL(new File(policyFile.getCanonicalPath())); + } else { + policyURL = newURL(extra_policy); + } + if (init(policyURL, newInfo)) { + loaded_policy = true; + } + } catch (Exception e) {} + if (overrideAll) { + return Boolean.valueOf(loaded_policy); + } + } + } + + int n = 1; + String policy_uri; + + while ((policy_uri = Security.getProperty(urlname + n)) != null) { + try { + URL policy_url = null; + String expanded_uri = PropertyExpander.expand(policy_uri).replace(File.separatorChar, '/'); + + if (policy_uri.startsWith("file:${java.home}/") || policy_uri.startsWith("file:${user.home}/")) { + + // this special case accommodates + // the situation java.home/user.home + // expand to a single slash, resulting in + // a file://foo URI + policy_url = new File(expanded_uri.substring(5)).toURI().toURL(); + } else { + policy_url = new URI(expanded_uri).toURL(); + } + + if (init(policy_url, newInfo)) { + loaded_policy = true; + } + } catch (Exception e) { + // ignore that policy + } + n++; + } + return Boolean.valueOf(loaded_policy); + } + + /** + * Reads a policy configuration into the Policy object using a + * Reader object. + */ + private boolean init(URL policy, PolicyInfo newInfo) { + + // skip parsing policy file if it has been previously parsed and + // has syntax errors + if (badPolicyURLs.contains(policy)) { + return false; + } + + try (InputStreamReader isr = getInputStreamReader(PolicyUtil.getInputStream(policy))) { + + PolicyParser pp = new PolicyParser(expandProperties); + pp.read(isr); + + KeyStore keyStore = null; + try { + keyStore = PolicyUtil.getKeyStore( + policy, + pp.getKeyStoreUrl(), + pp.getKeyStoreType(), + pp.getKeyStoreProvider(), + pp.getStorePassURL() + ); + } catch (Exception e) { + // ignore, treat it like we have no keystore + } + + Enumeration enum_ = pp.grantElements(); + while (enum_.hasMoreElements()) { + PolicyParser.GrantEntry ge = enum_.nextElement(); + addGrantEntry(ge, keyStore, newInfo); + } + return true; + } catch (PolicyParser.ParsingException pe) { + // record bad policy file to avoid later reparsing it + badPolicyURLs.add(policy); + pe.printStackTrace(System.err); + } catch (Exception e) {} + + return false; + } + + private InputStreamReader getInputStreamReader(InputStream is) { + /* + * Read in policy using UTF-8 by default. + * + * Check non-standard system property to see if the default encoding + * should be used instead. + */ + return (notUtf8) ? new InputStreamReader(is) : new InputStreamReader(is, UTF_8); + } + + private void initStaticPolicy(final PolicyInfo newInfo) { + PolicyEntry pe = new PolicyEntry(new CodeSource(null, (Certificate[]) null)); + pe.add(SecurityConstants.LOCAL_LISTEN_PERMISSION); + pe.add(new PropertyPermission("java.version", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.vendor", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.vendor.url", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.class.version", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("os.name", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("os.version", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("os.arch", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("file.separator", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("path.separator", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("line.separator", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.specification.version", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.specification.maintenance.version", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.specification.vendor", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.specification.name", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.vm.specification.version", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.vm.specification.vendor", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.vm.specification.name", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.vm.version", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.vm.vendor", SecurityConstants.PROPERTY_READ_ACTION)); + pe.add(new PropertyPermission("java.vm.name", SecurityConstants.PROPERTY_READ_ACTION)); + + // No need to sync because no one has access to newInfo yet + newInfo.policyEntries.add(pe); + } + + /** + * Given a GrantEntry, create a codeSource. + * + * @return null if signedBy alias is not recognized + */ + private CodeSource getCodeSource(PolicyParser.GrantEntry ge, KeyStore keyStore, PolicyInfo newInfo) + throws java.net.MalformedURLException { + Certificate[] certs = null; + if (ge.signedBy != null) { + certs = getCertificates(keyStore, ge.signedBy, newInfo); + if (certs == null) { + return null; + } + } + + URL location; + + if (ge.codeBase != null) location = newURL(ge.codeBase); + else location = null; + + return (canonicalizeCodebase(new CodeSource(location, certs), false)); + } + + /** + * Add one policy entry to the list. + */ + private void addGrantEntry(PolicyParser.GrantEntry ge, KeyStore keyStore, PolicyInfo newInfo) { + + try { + CodeSource codesource = getCodeSource(ge, keyStore, newInfo); + // skip if signedBy alias was unknown... + if (codesource == null) return; + + // perform keystore alias principal replacement. + // for example, if alias resolves to X509 certificate, + // replace principal with: + // -- skip if alias is unknown + if (replacePrincipals(ge.principals, keyStore) == false) return; + PolicyEntry entry = new PolicyEntry(codesource, ge.principals); + Enumeration enum_ = ge.permissionElements(); + while (enum_.hasMoreElements()) { + PolicyParser.PermissionEntry pe = enum_.nextElement(); + + try { + // perform ${{ ... }} expansions within permission name + expandPermissionName(pe, keyStore); + + // XXX special case PrivateCredentialPermission-SELF + Permission perm; + if (pe.permission.equals("javax.security.auth.PrivateCredentialPermission") && pe.name.endsWith(" self")) { + pe.name = pe.name.substring(0, pe.name.indexOf("self")) + SELF; + } + // check for self + if (pe.name != null && pe.name.contains(SELF)) { + // Create a "SelfPermission" , it could be an + // an unresolved permission which will be resolved + // when implies is called + // Add it to entry + Certificate[] certs; + if (pe.signedBy != null) { + certs = getCertificates(keyStore, pe.signedBy, newInfo); + } else { + certs = null; + } + perm = new SelfPermission(pe.permission, pe.name, pe.action, certs); + } else { + perm = getInstance(pe.permission, pe.name, pe.action); + } + entry.add(perm); + } catch (ClassNotFoundException cnfe) { + Certificate[] certs; + if (pe.signedBy != null) { + certs = getCertificates(keyStore, pe.signedBy, newInfo); + } else { + certs = null; + } + + // only add if we had no signer or we had + // a signer and found the keys for it. + if (certs != null || pe.signedBy == null) { + Permission perm = new UnresolvedPermission(pe.permission, pe.name, pe.action, certs); + entry.add(perm); + } + } catch (java.lang.reflect.InvocationTargetException ite) { + ite.printStackTrace(System.err); + } catch (Exception e) { + e.printStackTrace(System.err); + } + } + + // No need to sync because no one has access to newInfo yet + newInfo.policyEntries.add(entry); + } catch (Exception e) { + e.printStackTrace(System.err); + } + } + + /** + * Returns a new Permission object of the given Type. The Permission is + * created by getting the + * Class object using the Class.forName method, and using + * the reflection API to invoke the (String name, String actions) + * constructor on the + * object. + * + * @param type the type of Permission being created. + * @param name the name of the Permission being created. + * @param actions the actions of the Permission being created. + * + * @exception ClassNotFoundException if the particular Permission + * class could not be found. + * + * @exception IllegalAccessException if the class or initializer is + * not accessible. + * + * @exception InstantiationException if getInstance tries to + * instantiate an abstract class or an interface, or if the + * instantiation fails for some other reason. + * + * @exception NoSuchMethodException if the (String, String) constructor + * is not found. + * + * @exception InvocationTargetException if the underlying Permission + * constructor throws an exception. + * + */ + + private static final Permission getInstance(String type, String name, String actions) throws ClassNotFoundException, + InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + Class pc = Class.forName(type, false, null); + Permission answer = getKnownPermission(pc, name, actions); + if (answer != null) { + return answer; + } + if (!Permission.class.isAssignableFrom(pc)) { + // not the right subtype + throw new ClassCastException(type + " is not a Permission"); + } + + if (name == null && actions == null) { + try { + Constructor c = pc.getConstructor(PARAMS0); + return (Permission) c.newInstance(new Object[] {}); + } catch (NoSuchMethodException ne) { + try { + Constructor c = pc.getConstructor(PARAMS1); + return (Permission) c.newInstance(new Object[] { name }); + } catch (NoSuchMethodException ne1) { + Constructor c = pc.getConstructor(PARAMS2); + return (Permission) c.newInstance(new Object[] { name, actions }); + } + } + } else { + if (name != null && actions == null) { + try { + Constructor c = pc.getConstructor(PARAMS1); + return (Permission) c.newInstance(new Object[] { name }); + } catch (NoSuchMethodException ne) { + Constructor c = pc.getConstructor(PARAMS2); + return (Permission) c.newInstance(new Object[] { name, actions }); + } + } else { + Constructor c = pc.getConstructor(PARAMS2); + return (Permission) c.newInstance(new Object[] { name, actions }); + } + } + } + + /** + * Creates one of the well-known permissions in the java.base module + * directly instead of via reflection. Keep list short to not penalize + * permissions from other modules. + */ + private static Permission getKnownPermission(Class claz, String name, String actions) { + if (claz.equals(FilePermission.class)) { + return new FilePermission(name, actions); + } else if (claz.equals(SocketPermission.class)) { + return new SocketPermission(name, actions); + } else if (claz.equals(RuntimePermission.class)) { + return new RuntimePermission(name, actions); + } else if (claz.equals(PropertyPermission.class)) { + return new PropertyPermission(name, actions); + } else if (claz.equals(NetPermission.class)) { + return new NetPermission(name, actions); + } else if (claz.equals(AllPermission.class)) { + return SecurityConstants.ALL_PERMISSION; + } else if (claz.equals(SecurityPermission.class)) { + return new SecurityPermission(name, actions); + } else { + return null; + } + } + + /** + * Creates one of the well-known principals in the java.base module + * directly instead of via reflection. Keep list short to not penalize + * principals from other modules. + */ + private static Principal getKnownPrincipal(Class claz, String name) { + if (claz.equals(X500Principal.class)) { + return new X500Principal(name); + } else { + return null; + } + } + + /** + * Fetch all certs associated with this alias. + */ + private Certificate[] getCertificates(KeyStore keyStore, String aliases, PolicyInfo newInfo) { + + List vcerts = null; + + StringTokenizer st = new StringTokenizer(aliases, ","); + int n = 0; + + while (st.hasMoreTokens()) { + String alias = st.nextToken().trim(); + n++; + Certificate cert = null; + // See if this alias's cert has already been cached + synchronized (newInfo.aliasMapping) { + cert = (Certificate) newInfo.aliasMapping.get(alias); + + if (cert == null && keyStore != null) { + + try { + cert = keyStore.getCertificate(alias); + } catch (KeyStoreException kse) { + // never happens, because keystore has already been loaded + // when we call this + } + if (cert != null) { + newInfo.aliasMapping.put(alias, cert); + newInfo.aliasMapping.put(cert, alias); + } + } + } + + if (cert != null) { + if (vcerts == null) vcerts = new ArrayList<>(); + vcerts.add(cert); + } + } + + // make sure n == vcerts.size, since we are doing a logical *and* + if (vcerts != null && n == vcerts.size()) { + Certificate[] certs = new Certificate[vcerts.size()]; + vcerts.toArray(certs); + return certs; + } else { + return null; + } + } + + /** + * Refreshes the policy object by re-reading all the policy files. + */ + @Override + public void refresh() { + init(url); + } + + /** + * Evaluates the global policy for the permissions granted to + * the ProtectionDomain and tests whether the permission is + * granted. + * + * @param pd the ProtectionDomain to test + * @param p the Permission object to be tested for implication. + * + * @return true if "permission" is a proper subset of a permission + * granted to this ProtectionDomain. + * + * @see java.security.ProtectionDomain + */ + @Override + public boolean implies(ProtectionDomain pd, Permission p) { + PermissionCollection pc = getPermissions(pd); + if (pc == null) { + return false; + } + + // cache mapping of protection domain to its PermissionCollection + return pc.implies(p); + } + + /** + * Examines this Policy and returns the permissions granted + * to the specified ProtectionDomain. This includes + * the permissions currently associated with the domain as well + * as the policy permissions granted to the domain's + * CodeSource, ClassLoader, and Principals. + * + *

Note that this Policy implementation has + * special handling for PrivateCredentialPermissions. + * When this method encounters a PrivateCredentialPermission + * which specifies "self" as the Principal class and name, + * it does not add that Permission to the returned + * PermissionCollection. Instead, it builds + * a new PrivateCredentialPermission + * for each Principal associated with the provided + * Subject. Each new PrivateCredentialPermission + * contains the same Credential class as specified in the + * originally granted permission, as well as the Class and name + * for the respective Principal. + * + * @param domain the Permissions granted to this + * ProtectionDomain are returned. + * + * @return the Permissions granted to the provided + * ProtectionDomain. + */ + @Override + public PermissionCollection getPermissions(ProtectionDomain domain) { + Permissions perms = new Permissions(); + + if (domain == null) return perms; + + // first get policy perms + getPermissions(perms, domain); + + // add static perms + // - adding static perms after policy perms is necessary + // to avoid a regression for 4301064 + PermissionCollection pc = domain.getPermissions(); + if (pc != null) { + synchronized (pc) { + Enumeration e = pc.elements(); + while (e.hasMoreElements()) { + perms.add(e.nextElement()); + } + } + } + + return perms; + } + + /** + * Examines this Policy and creates a PermissionCollection object with + * the set of permissions for the specified CodeSource. + * + * @param codesource the CodeSource associated with the caller. + * This encapsulates the original location of the code (where the code + * came from) and the public key(s) of its signer. + * + * @return the set of permissions according to the policy. + */ + @Override + public PermissionCollection getPermissions(CodeSource codesource) { + return getPermissions(new Permissions(), codesource); + } + + /** + * Examines the global policy and returns the provided Permissions + * object with additional permissions granted to the specified + * ProtectionDomain. + * + * @param perms the Permissions to populate + * @param pd the ProtectionDomain associated with the caller. + * + * @return the set of Permissions according to the policy. + */ + private PermissionCollection getPermissions(Permissions perms, ProtectionDomain pd) { + final CodeSource cs = pd.getCodeSource(); + if (cs == null) return perms; + + CodeSource canonCodeSource = canonicalizeCodebase(cs, true); + return getPermissions(perms, canonCodeSource, pd.getPrincipals()); + } + + /** + * Examines the global policy and returns the provided Permissions + * object with additional permissions granted to the specified + * CodeSource. + * + * @param perms the permissions to populate + * @param cs the codesource associated with the caller. + * This encapsulates the original location of the code (where the code + * came from) and the public key(s) of its signer. + * + * @return the set of permissions according to the policy. + */ + private PermissionCollection getPermissions(Permissions perms, final CodeSource cs) { + + if (cs == null) return perms; + + CodeSource canonCodeSource = canonicalizeCodebase(cs, true); + return getPermissions(perms, canonCodeSource, null); + } + + private Permissions getPermissions(Permissions perms, final CodeSource cs, Principal[] principals) { + for (PolicyEntry entry : policyInfo.policyEntries) { + addPermissions(perms, cs, principals, entry); + } + + return perms; + } + + private void addPermissions(Permissions perms, final CodeSource cs, Principal[] principals, final PolicyEntry entry) { + + // check to see if the CodeSource implies + Boolean imp = entry.getCodeSource().implies(cs); + if (!imp.booleanValue()) { + // CodeSource does not imply - return and try next policy entry + return; + } + + // check to see if the Principals imply + + List entryPs = entry.getPrincipals(); + + if (entryPs == null || entryPs.isEmpty()) { + + // policy entry has no principals - + // add perms regardless of principals in current ACC + + addPerms(perms, principals, entry); + return; + + } else if (principals == null || principals.length == 0) { + + // current thread has no principals but this policy entry + // has principals - perms are not added + + return; + } + + // current thread has principals and this policy entry + // has principals. see if policy entry principals match + // principals in current ACC + + for (PolicyParser.PrincipalEntry pppe : entryPs) { + + // Check for wildcards + if (pppe.isWildcardClass()) { + // a wildcard class matches all principals in current ACC + continue; + } + + if (pppe.isWildcardName()) { + // a wildcard name matches any principal with the same class + if (wildcardPrincipalNameImplies(pppe.principalClass, principals)) { + continue; + } + // policy entry principal not in current ACC - + // immediately return and go to next policy entry + return; + } + + Set pSet = new HashSet<>(Arrays.asList(principals)); + Subject subject = new Subject(true, pSet, Collections.EMPTY_SET, Collections.EMPTY_SET); + try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Class pClass = Class.forName(pppe.principalClass, false, cl); + Principal p = getKnownPrincipal(pClass, pppe.principalName); + if (p == null) { + if (!Principal.class.isAssignableFrom(pClass)) { + // not the right subtype + throw new ClassCastException(pppe.principalClass + " is not a Principal"); + } + + Constructor c = pClass.getConstructor(PARAMS1); + p = (Principal) c.newInstance(new Object[] { pppe.principalName }); + + } + + // check if the Principal implies the current + // thread's principals + if (!p.implies(subject)) { + // policy principal does not imply the current Subject - + // immediately return and go to next policy entry + return; + } + } catch (Exception e) { + // fall back to default principal comparison. + // see if policy entry principal is in current ACC + + if (!pppe.implies(subject)) { + // policy entry principal not in current ACC - + // immediately return and go to next policy entry + return; + } + } + + // either the principal information matched, + // or the Principal.implies succeeded. + // continue loop and test the next policy principal + } + + // all policy entry principals were found in the current ACC - + // grant the policy permissions + + addPerms(perms, principals, entry); + } + + /** + * Returns true if the array of principals contains at least one + * principal of the specified class. + */ + private static boolean wildcardPrincipalNameImplies(String principalClass, Principal[] principals) { + for (Principal p : principals) { + if (principalClass.equals(p.getClass().getName())) { + return true; + } + } + return false; + } + + private void addPerms(Permissions perms, Principal[] accPs, PolicyEntry entry) { + for (int i = 0; i < entry.permissions.size(); i++) { + Permission p = entry.permissions.get(i); + + if (p instanceof SelfPermission) { + // handle "SELF" permissions + expandSelf((SelfPermission) p, entry.getPrincipals(), accPs, perms); + } else { + perms.add(p); + } + } + } + + /** + * @param sp the SelfPermission that needs to be expanded. + * + * @param entryPs list of principals for the Policy entry. + * + * @param pdp Principal array from the current ProtectionDomain. + * + * @param perms the PermissionCollection where the individual + * Permissions will be added after expansion. + */ + + private void expandSelf(SelfPermission sp, List entryPs, Principal[] pdp, Permissions perms) { + + if (entryPs == null || entryPs.isEmpty()) { + return; + } + int startIndex = 0; + int v; + StringBuilder sb = new StringBuilder(); + while ((v = sp.getSelfName().indexOf(SELF, startIndex)) != -1) { + + // add non-SELF string + sb.append(sp.getSelfName().substring(startIndex, v)); + + // expand SELF + Iterator pli = entryPs.iterator(); + while (pli.hasNext()) { + PolicyParser.PrincipalEntry pppe = pli.next(); + String[][] principalInfo = getPrincipalInfo(pppe, pdp); + for (int i = 0; i < principalInfo.length; i++) { + if (i != 0) { + sb.append(", "); + } + sb.append(principalInfo[i][0] + " " + "\"" + principalInfo[i][1] + "\""); + } + if (pli.hasNext()) { + sb.append(", "); + } + } + startIndex = v + SELF.length(); + } + // add remaining string (might be the entire string) + sb.append(sp.getSelfName().substring(startIndex)); + + try { + // first try to instantiate the permission + perms.add(getInstance(sp.getSelfType(), sb.toString(), sp.getSelfActions())); + } catch (ClassNotFoundException cnfe) { + // ok, the permission is not in the bootclasspath. + // before we add an UnresolvedPermission, check to see + // whether this perm already belongs to the collection. + // if so, use that perm's ClassLoader to create a new + // one. + Class pc = null; + synchronized (perms) { + Enumeration e = perms.elements(); + while (e.hasMoreElements()) { + Permission pElement = e.nextElement(); + if (pElement.getClass().getName().equals(sp.getSelfType())) { + pc = pElement.getClass(); + break; + } + } + } + if (pc == null) { + // create an UnresolvedPermission + perms.add(new UnresolvedPermission(sp.getSelfType(), sb.toString(), sp.getSelfActions(), sp.getCerts())); + } else { + try { + // we found an instantiated permission. + // use its class loader to instantiate a new permission. + Constructor c; + // name parameter can not be null + if (sp.getSelfActions() == null) { + try { + c = pc.getConstructor(PARAMS1); + perms.add((Permission) c.newInstance(new Object[] { sb.toString() })); + } catch (NoSuchMethodException ne) { + c = pc.getConstructor(PARAMS2); + perms.add((Permission) c.newInstance(new Object[] { sb.toString(), sp.getSelfActions() })); + } + } else { + c = pc.getConstructor(PARAMS2); + perms.add((Permission) c.newInstance(new Object[] { sb.toString(), sp.getSelfActions() })); + } + } catch (Exception nme) {} + } + } catch (Exception e) {} + } + + /** + * return the principal class/name pair in the 2D array. + * array[x][y]: x corresponds to the array length. + * if (y == 0), it's the principal class. + * if (y == 1), it's the principal name. + */ + private String[][] getPrincipalInfo(PolicyParser.PrincipalEntry pe, Principal[] pdp) { + + // there are 3 possibilities: + // 1) the entry's Principal class and name are not wildcarded + // 2) the entry's Principal name is wildcarded only + // 3) the entry's Principal class and name are wildcarded + + if (!pe.isWildcardClass() && !pe.isWildcardName()) { + + // build an info array for the principal + // from the Policy entry + String[][] info = new String[1][2]; + info[0][0] = pe.principalClass; + info[0][1] = pe.principalName; + return info; + + } else if (!pe.isWildcardClass() && pe.isWildcardName()) { + + // build an info array for every principal + // in the current domain which has a principal class + // that is equal to policy entry principal class name + List plist = new ArrayList<>(); + for (int i = 0; i < pdp.length; i++) { + if (pe.principalClass.equals(pdp[i].getClass().getName())) plist.add(pdp[i]); + } + String[][] info = new String[plist.size()][2]; + int i = 0; + for (Principal p : plist) { + info[i][0] = p.getClass().getName(); + info[i][1] = p.getName(); + i++; + } + return info; + + } else { + + // build an info array for every + // one of the current Domain's principals + + String[][] info = new String[pdp.length][2]; + + for (int i = 0; i < pdp.length; i++) { + info[i][0] = pdp[i].getClass().getName(); + info[i][1] = pdp[i].getName(); + } + return info; + } + } + + /* + * Returns the signer certificates from the list of certificates + * associated with the given code source. + * + * The signer certificates are those certificates that were used + * to verify signed code originating from the codesource location. + * + * This method assumes that in the given code source, each signer + * certificate is followed by its supporting certificate chain + * (which may be empty), and that the signer certificate and its + * supporting certificate chain are ordered bottom-to-top + * (i.e., with the signer certificate first and the (root) certificate + * authority last). + */ + protected Certificate[] getSignerCertificates(CodeSource cs) { + Certificate[] certs = null; + if ((certs = cs.getCertificates()) == null) return null; + for (int i = 0; i < certs.length; i++) { + if (!(certs[i] instanceof X509Certificate)) return cs.getCertificates(); + } + + // Do we have to do anything? + int i = 0; + int count = 0; + while (i < certs.length) { + count++; + while (((i + 1) < certs.length) + && ((X509Certificate) certs[i]).getIssuerX500Principal() + .equals(((X509Certificate) certs[i + 1]).getSubjectX500Principal())) { + i++; + } + i++; + } + if (count == certs.length) + // Done + return certs; + + List userCertList = new ArrayList<>(); + i = 0; + while (i < certs.length) { + userCertList.add(certs[i]); + while (((i + 1) < certs.length) + && ((X509Certificate) certs[i]).getIssuerX500Principal() + .equals(((X509Certificate) certs[i + 1]).getSubjectX500Principal())) { + i++; + } + i++; + } + Certificate[] userCerts = new Certificate[userCertList.size()]; + userCertList.toArray(userCerts); + return userCerts; + } + + private CodeSource canonicalizeCodebase(CodeSource cs, boolean extractSignerCerts) { + + String path = null; + + CodeSource canonCs = cs; + URL u = cs.getLocation(); + if (u != null) { + if (u.getProtocol().equals("jar")) { + // unwrap url embedded inside jar url + String spec = u.getFile(); + int separator = spec.indexOf("!/"); + if (separator != -1) { + try { + u = newURL(spec.substring(0, separator)); + } catch (MalformedURLException e) { + // Fail silently. In this case, url stays what + // it was above + } + } + } + if (u.getProtocol().equals("file")) { + boolean isLocalFile = false; + String host = u.getHost(); + isLocalFile = (host == null || host.isEmpty() || host.equals("~") || host.equalsIgnoreCase("localhost")); + + if (isLocalFile) { + path = u.getFile().replace('/', File.separatorChar); + path = ParseUtil.decode(path); + } + } + } + + if (path != null) { + try { + URL csUrl = null; + path = canonPath(path); + csUrl = ParseUtil.fileToEncodedURL(new File(path)); + + if (extractSignerCerts) { + canonCs = new CodeSource(csUrl, getSignerCertificates(cs)); + } else { + canonCs = new CodeSource(csUrl, cs.getCertificates()); + } + } catch (IOException ioe) { + // leave codesource as it is, unless we have to extract its + // signer certificates + if (extractSignerCerts) { + canonCs = new CodeSource(cs.getLocation(), getSignerCertificates(cs)); + } + } + } else { + if (extractSignerCerts) { + canonCs = new CodeSource(cs.getLocation(), getSignerCertificates(cs)); + } + } + return canonCs; + } + + // Wrapper to return a canonical path that avoids calling getCanonicalPath() + // with paths that are intended to match all entries in the directory + private static String canonPath(String path) throws IOException { + if (path.endsWith("*")) { + path = path.substring(0, path.length() - 1) + "-"; + path = new File(path).getCanonicalPath(); + return path.substring(0, path.length() - 1) + "*"; + } else { + return new File(path).getCanonicalPath(); + } + } + + /** + * return true if no replacement was performed, + * or if replacement succeeded. + */ + private boolean replacePrincipals(List principals, KeyStore keystore) { + + if (principals == null || principals.isEmpty() || keystore == null) return true; + + for (PolicyParser.PrincipalEntry pppe : principals) { + if (pppe.isReplaceName()) { + + // perform replacement + // (only X509 replacement is possible now) + String name; + if ((name = getDN(pppe.principalName, keystore)) == null) { + return false; + } + + pppe.principalClass = X500PRINCIPAL; + pppe.principalName = name; + } + } + // return true if no replacement was performed, + // or if replacement succeeded + return true; + } + + private void expandPermissionName(PolicyParser.PermissionEntry pe, KeyStore keystore) throws Exception { + // short cut the common case + if (pe.name == null || pe.name.indexOf("${{", 0) == -1) { + return; + } + + int startIndex = 0; + int b, e; + StringBuilder sb = new StringBuilder(); + while ((b = pe.name.indexOf("${{", startIndex)) != -1) { + e = pe.name.indexOf("}}", b); + if (e < 1) { + break; + } + sb.append(pe.name.substring(startIndex, b)); + + // get the value in ${{...}} + String value = pe.name.substring(b + 3, e); + + // parse up to the first ':' + int colonIndex; + String prefix = value; + String suffix; + if ((colonIndex = value.indexOf(':')) != -1) { + prefix = value.substring(0, colonIndex); + } + + // handle different prefix possibilities + if (prefix.equalsIgnoreCase("self")) { + // do nothing - handled later + sb.append(pe.name.substring(b, e + 2)); + startIndex = e + 2; + continue; + } else if (prefix.equalsIgnoreCase("alias")) { + // get the suffix and perform keystore alias replacement + if (colonIndex == -1) { + throw new Exception("Alias name not provided pe.name: " + pe.name); + } + suffix = value.substring(colonIndex + 1); + if ((suffix = getDN(suffix, keystore)) == null) { + throw new Exception("Unable to perform substitution on alias suffix: " + value.substring(colonIndex + 1)); + } + + sb.append(X500PRINCIPAL + " \"" + suffix + "\""); + startIndex = e + 2; + } else { + throw new Exception("Substitution value prefix unsupported: " + prefix); + } + } + + // copy the rest of the value + sb.append(pe.name.substring(startIndex)); + + pe.name = sb.toString(); + } + + private String getDN(String alias, KeyStore keystore) { + Certificate cert = null; + try { + cert = keystore.getCertificate(alias); + } catch (Exception e) { + return null; + } + + if (!(cert instanceof X509Certificate x509Cert)) { + return null; + } else { + // 4702543: X500 names with an EmailAddress + // were encoded incorrectly. create new + // X500Principal name with correct encoding + + X500Principal p = new X500Principal(x509Cert.getSubjectX500Principal().toString()); + return p.getName(); + } + } + + /** + * Each entry in the policy configuration file is represented by a + * PolicyEntry object.

+ * + * A PolicyEntry is a (CodeSource,Permission) pair. The + * CodeSource contains the (URL, PublicKey) that together identify + * where the Java bytecodes come from and who (if anyone) signed + * them. The URL could refer to localhost. The URL could also be + * null, meaning that this policy entry is given to all comers, as + * long as they match the signer field. The signer could be null, + * meaning the code is not signed.

+ * + * The Permission contains the (Type, Name, Action) triplet.

+ * + * For now, the Policy object retrieves the public key from the + * X.509 certificate on disk that corresponds to the signedBy + * alias specified in the Policy config file. For reasons of + * efficiency, the Policy object keeps a hashtable of certs already + * read in. This could be replaced by a secure internal key + * store. + * + *

+ * For example, the entry + *

+     *          permission java.io.File "/tmp", "read,write",
+     *          signedBy "Duke";
+     * 
+ * is represented internally + *
+     *
+     * FilePermission f = new FilePermission("/tmp", "read,write");
+     * PublicKey p = publickeys.get("Duke");
+     * URL u = InetAddress.getLocalHost();
+     * CodeBase c = new CodeBase( p, u );
+     * pe = new PolicyEntry(f, c);
+     * 
+ * + * @author Marianne Mueller + * @author Roland Schemers + * @see java.security.CodeSource + * @see java.security.Policy + * @see java.security.Permissions + * @see java.security.ProtectionDomain + */ + private static class PolicyEntry { + + private final CodeSource codesource; + final List permissions; + private final List principals; + + /** + * Given a Permission and a CodeSource, create a policy entry. + * + * XXX Decide if/how to add validity fields and "purpose" fields to + * XXX policy entries + * + * @param cs the CodeSource, which encapsulates the URL and the + * public key + * attributes from the policy config file. Validity checks + * are performed on the public key before PolicyEntry is + * called. + * + */ + PolicyEntry(CodeSource cs, List principals) { + this.codesource = cs; + this.permissions = new ArrayList(); + this.principals = principals; // can be null + } + + PolicyEntry(CodeSource cs) { + this(cs, null); + } + + List getPrincipals() { + return principals; // can be null + } + + /** + * add a Permission object to this entry. + * No need to sync add op because perms are added to entry only + * while entry is being initialized + */ + void add(Permission p) { + permissions.add(p); + } + + /** + * Return the CodeSource for this policy entry + */ + CodeSource getCodeSource() { + return codesource; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + sb.append(getCodeSource()); + sb.append("\n"); + for (int j = 0; j < permissions.size(); j++) { + Permission p = permissions.get(j); + sb.append(" "); + sb.append(" "); + sb.append(p); + sb.append("\n"); + } + sb.append("}"); + sb.append("\n"); + return sb.toString(); + } + } + + private static class SelfPermission extends Permission { + + @java.io.Serial + private static final long serialVersionUID = -8315562579967246806L; + + /** + * The class name of the Permission class that will be + * created when this self permission is expanded . + * + * @serial + */ + private String type; + + /** + * The permission name. + * + * @serial + */ + private String name; + + /** + * The actions of the permission. + * + * @serial + */ + private String actions; + + /** + * The certs of the permission. + * + * @serial + */ + private Certificate[] certs; + + /** + * Creates a new SelfPermission containing the permission + * information needed later to expand the self + * @param type the class name of the Permission class that will be + * created when this permission is expanded and if necessary resolved. + * @param name the name of the permission. + * @param actions the actions of the permission. + * @param certs the certificates the permission's class was signed with. + * This is a list of certificate chains, where each chain is composed of + * a signer certificate and optionally its supporting certificate chain. + * Each chain is ordered bottom-to-top (i.e., with the signer + * certificate first and the (root) certificate authority last). + */ + public SelfPermission(String type, String name, String actions, Certificate[] certs) { + super(type); + if (type == null) { + throw new NullPointerException("Ttype cannot be null"); + } + this.type = type; + this.name = name; + this.actions = actions; + if (certs != null) { + // Extract the signer certs from the list of certificates. + for (int i = 0; i < certs.length; i++) { + if (!(certs[i] instanceof X509Certificate)) { + // there is no concept of signer certs, so we store the + // entire cert array + this.certs = certs.clone(); + break; + } + } + + if (this.certs == null) { + // Go through the list of certs and see if all the certs are + // signer certs. + int i = 0; + int count = 0; + while (i < certs.length) { + count++; + while (((i + 1) < certs.length) + && ((X509Certificate) certs[i]).getIssuerX500Principal() + .equals(((X509Certificate) certs[i + 1]).getSubjectX500Principal())) { + i++; + } + i++; + } + if (count == certs.length) { + // All the certs are signer certs, so we store the + // entire array + this.certs = certs.clone(); + } + + if (this.certs == null) { + // extract the signer certs + List signerCerts = new ArrayList<>(); + i = 0; + while (i < certs.length) { + signerCerts.add(certs[i]); + while (((i + 1) < certs.length) + && ((X509Certificate) certs[i]).getIssuerX500Principal() + .equals(((X509Certificate) certs[i + 1]).getSubjectX500Principal())) { + i++; + } + i++; + } + this.certs = new Certificate[signerCerts.size()]; + signerCerts.toArray(this.certs); + } + } + } + } + + /** + * This method always returns false for SelfPermission permissions. + * That is, an SelfPermission never considered to + * imply another permission. + * + * @param p the permission to check against. + * + * @return false. + */ + @Override + public boolean implies(Permission p) { + return false; + } + + /** + * Checks two SelfPermission objects for equality. + * + * Checks that obj is an SelfPermission, and has + * the same type (class) name, permission name, actions, and + * certificates as this object. + * + * @param obj the object we are testing for equality with this object. + * + * @return true if obj is an SelfPermission, and has the same + * type (class) name, permission name, actions, and + * certificates as this object. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + + if (!(obj instanceof SelfPermission)) return false; + SelfPermission that = (SelfPermission) obj; + + if (!(this.type.equals(that.type) && this.name.equals(that.name) && this.actions.equals(that.actions))) return false; + + if ((this.certs == null) && (that.certs == null)) { + return true; + } + + if ((this.certs == null) || (that.certs == null)) { + return false; + } + + if (this.certs.length != that.certs.length) { + return false; + } + + int i, j; + boolean match; + + for (i = 0; i < this.certs.length; i++) { + match = false; + for (j = 0; j < that.certs.length; j++) { + if (this.certs[i].equals(that.certs[j])) { + match = true; + break; + } + } + if (!match) return false; + } + + for (i = 0; i < that.certs.length; i++) { + match = false; + for (j = 0; j < this.certs.length; j++) { + if (that.certs[i].equals(this.certs[j])) { + match = true; + break; + } + } + if (!match) return false; + } + return true; + } + + /** + * Returns the hash code value for this object. + * + * @return a hash code value for this object. + */ + @Override + public int hashCode() { + int hash = type.hashCode(); + if (name != null) hash ^= name.hashCode(); + if (actions != null) hash ^= actions.hashCode(); + return hash; + } + + /** + * Returns the canonical string representation of the actions, + * which currently is the empty string "", since there are no actions + * for an SelfPermission. That is, the actions for the + * permission that will be created when this SelfPermission + * is resolved may be non-null, but an SelfPermission + * itself is never considered to have any actions. + * + * @return the empty string "". + */ + @Override + public String getActions() { + return ""; + } + + public String getSelfType() { + return type; + } + + public String getSelfName() { + return name; + } + + public String getSelfActions() { + return actions; + } + + public Certificate[] getCerts() { + return (certs == null ? null : certs.clone()); + } + + /** + * Returns a string describing this SelfPermission. The convention + * is to specify the class name, the permission name, and the actions, + * in the following format: '(unresolved "ClassName" "name" "actions")'. + * + * @return information about this SelfPermission. + */ + @Override + public String toString() { + return "(SelfPermission " + type + " " + name + " " + actions + ")"; + } + + /** + * Restores the state of this object from the stream. + * + * @param stream the {@code ObjectInputStream} from which data is read + * @throws IOException if an I/O error occurs + * @throws ClassNotFoundException if a serialized class cannot be loaded + */ + @java.io.Serial + private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { + stream.defaultReadObject(); + if (certs != null) { + this.certs = certs.clone(); + } + } + } + + /** + * holds policy information that we need to synch on + */ + private static class PolicyInfo { + // Stores grant entries in the policy + final List policyEntries; + + // Maps aliases to certs + final Map aliasMapping; + + PolicyInfo(int numCaches) { + policyEntries = new ArrayList<>(); + aliasMapping = Collections.synchronizedMap(new HashMap<>(11)); + } + } + + @SuppressWarnings("deprecation") + private static URL newURL(String spec) throws MalformedURLException { + return new URL(spec); + } +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java new file mode 100644 index 0000000000000..9d5b0d5a13722 --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java @@ -0,0 +1,1163 @@ +/* + * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +import javax.security.auth.x500.X500Principal; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StreamTokenizer; +import java.io.Writer; +import java.security.GeneralSecurityException; +import java.security.Principal; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.TreeMap; +import java.util.Vector; + +/** + * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/provider/PolicyParser.java + */ +public class PolicyParser { + + private final Vector grantEntries; + private Map domainEntries; + + private StreamTokenizer st; + private int lookahead; + private boolean expandProp = false; + private String keyStoreUrlString = null; // unexpanded + private String keyStoreType = null; + private String keyStoreProvider = null; + private String storePassURL = null; + + private String expand(String value) throws PropertyExpander.ExpandException { + return expand(value, false); + } + + private String expand(String value, boolean encodeURL) throws PropertyExpander.ExpandException { + if (!expandProp) { + return value; + } else { + return PropertyExpander.expand(value, encodeURL); + } + } + + /** + * Creates a PolicyParser object. + */ + + public PolicyParser() { + grantEntries = new Vector<>(); + } + + public PolicyParser(boolean expandProp) { + this(); + this.expandProp = expandProp; + } + + /** + * Reads a policy configuration into the Policy object using a + * Reader object. + * + * @param policy the policy Reader object. + * + * @exception ParsingException if the policy configuration contains + * a syntax error. + * + * @exception IOException if an error occurs while reading the policy + * configuration. + */ + + public void read(Reader policy) throws ParsingException, IOException { + if (!(policy instanceof BufferedReader)) { + policy = new BufferedReader(policy); + } + + /* + * Configure the stream tokenizer: + * Recognize strings between "..." + * Don't convert words to lowercase + * Recognize both C-style and C++-style comments + * Treat end-of-line as white space, not as a token + */ + st = new StreamTokenizer(policy); + + st.resetSyntax(); + st.wordChars('a', 'z'); + st.wordChars('A', 'Z'); + st.wordChars('.', '.'); + st.wordChars('0', '9'); + st.wordChars('_', '_'); + st.wordChars('$', '$'); + st.wordChars(128 + 32, 255); + st.whitespaceChars(0, ' '); + st.commentChar('/'); + st.quoteChar('\''); + st.quoteChar('"'); + st.lowerCaseMode(false); + st.ordinaryChar('/'); + st.slashSlashComments(true); + st.slashStarComments(true); + + /* + * The main parsing loop. The loop is executed once + * for each entry in the config file. The entries + * are delimited by semicolons. Once we've read in + * the information for an entry, go ahead and try to + * add it to the policy vector. + * + */ + + lookahead = st.nextToken(); + GrantEntry ge = null; + while (lookahead != StreamTokenizer.TT_EOF) { + if (peek("grant")) { + ge = parseGrantEntry(); + // could be null if we couldn't expand a property + if (ge != null) add(ge); + } else if (peek("keystore") && keyStoreUrlString == null) { + // only one keystore entry per policy file, others will be + // ignored + parseKeyStoreEntry(); + } else if (peek("keystorePasswordURL") && storePassURL == null) { + // only one keystore passwordURL per policy file, others will be + // ignored + parseStorePassURL(); + } else if (ge == null && keyStoreUrlString == null && storePassURL == null && peek("domain")) { + if (domainEntries == null) { + domainEntries = new TreeMap<>(); + } + DomainEntry de = parseDomainEntry(); + String domainName = de.getName(); + if (domainEntries.putIfAbsent(domainName, de) != null) { + Object[] source = { domainName }; + String msg = "duplicate keystore domain name: " + domainName; + throw new ParsingException(msg, source); + } + } else { + // error? + } + match(";"); + } + + if (keyStoreUrlString == null && storePassURL != null) { + throw new ParsingException("Keystore Password URL cannot be specified without also specifying keystore"); + } + } + + public void add(GrantEntry ge) { + grantEntries.addElement(ge); + } + + public void replace(GrantEntry origGe, GrantEntry newGe) { + grantEntries.setElementAt(newGe, grantEntries.indexOf(origGe)); + } + + public boolean remove(GrantEntry ge) { + return grantEntries.removeElement(ge); + } + + /** + * Returns the (possibly expanded) keystore location, or null if the + * expansion fails. + */ + public String getKeyStoreUrl() { + try { + if (keyStoreUrlString != null && keyStoreUrlString.length() != 0) { + return expand(keyStoreUrlString, true).replace(File.separatorChar, '/'); + } + } catch (PropertyExpander.ExpandException peee) { + return null; + } + return null; + } + + public void setKeyStoreUrl(String url) { + keyStoreUrlString = url; + } + + public String getKeyStoreType() { + return keyStoreType; + } + + public void setKeyStoreType(String type) { + keyStoreType = type; + } + + public String getKeyStoreProvider() { + return keyStoreProvider; + } + + public void setKeyStoreProvider(String provider) { + keyStoreProvider = provider; + } + + public String getStorePassURL() { + try { + if (storePassURL != null && storePassURL.length() != 0) { + return expand(storePassURL, true).replace(File.separatorChar, '/'); + } + } catch (PropertyExpander.ExpandException peee) { + return null; + } + return null; + } + + public void setStorePassURL(String storePassURL) { + this.storePassURL = storePassURL; + } + + /** + * Enumerate all the entries in the global policy object. + * This method is used by policy admin tools. The tools + * should use the Enumeration methods on the returned object + * to fetch the elements sequentially. + */ + public Enumeration grantElements() { + return grantEntries.elements(); + } + + public Collection getDomainEntries() { + return domainEntries.values(); + } + + /** + * write out the policy + */ + + public void write(Writer policy) { + PrintWriter out = new PrintWriter(new BufferedWriter(policy)); + + out.println("/* AUTOMATICALLY GENERATED ON " + (new java.util.Date()) + "*/"); + out.println("/* DO NOT EDIT */"); + out.println(); + + // write the (unexpanded) keystore entry as the first entry of the + // policy file + if (keyStoreUrlString != null) { + writeKeyStoreEntry(out); + } + if (storePassURL != null) { + writeStorePassURL(out); + } + + // write "grant" entries + for (GrantEntry ge : grantEntries) { + ge.write(out); + out.println(); + } + out.flush(); + } + + /** + * parses a keystore entry + */ + private void parseKeyStoreEntry() throws ParsingException, IOException { + match("keystore"); + keyStoreUrlString = match("quoted string"); + + // parse keystore type + if (!peek(",")) { + return; // default type + } + match(","); + + if (peek("\"")) { + keyStoreType = match("quoted string"); + } else { + throw new ParsingException(st.lineno(), "Expected keystore type"); + } + + // parse keystore provider + if (!peek(",")) { + return; // provider optional + } + match(","); + + if (peek("\"")) { + keyStoreProvider = match("quoted string"); + } else { + throw new ParsingException(st.lineno(), "Keystore provider expected"); + } + } + + private void parseStorePassURL() throws ParsingException, IOException { + match("keyStorePasswordURL"); + storePassURL = match("quoted string"); + } + + /** + * writes the (unexpanded) keystore entry + */ + private void writeKeyStoreEntry(PrintWriter out) { + out.print("keystore \""); + out.print(keyStoreUrlString); + out.print('"'); + if (keyStoreType != null && !keyStoreType.isEmpty()) out.print(", \"" + keyStoreType + "\""); + if (keyStoreProvider != null && !keyStoreProvider.isEmpty()) out.print(", \"" + keyStoreProvider + "\""); + out.println(";"); + out.println(); + } + + private void writeStorePassURL(PrintWriter out) { + out.print("keystorePasswordURL \""); + out.print(storePassURL); + out.print('"'); + out.println(";"); + out.println(); + } + + /** + * parse a Grant entry + */ + private GrantEntry parseGrantEntry() throws ParsingException, IOException { + GrantEntry e = new GrantEntry(); + LinkedList principals = null; + boolean ignoreEntry = false; + + match("grant"); + + while (!peek("{")) { + + if (peekAndMatch("Codebase")) { + if (e.codeBase != null) throw new ParsingException(st.lineno(), "Multiple Codebase expressions"); + e.codeBase = match("quoted string"); + peekAndMatch(","); + } else if (peekAndMatch("SignedBy")) { + if (e.signedBy != null) throw new ParsingException(st.lineno(), "Multiple SignedBy expressions"); + e.signedBy = match("quoted string"); + + // verify syntax of the aliases + StringTokenizer aliases = new StringTokenizer(e.signedBy, ",", true); + int actr = 0; + int cctr = 0; + while (aliases.hasMoreTokens()) { + String alias = aliases.nextToken().trim(); + if (alias.equals(",")) cctr++; + else if (!alias.isEmpty()) actr++; + } + if (actr <= cctr) throw new ParsingException(st.lineno(), "SignedBy has an empty alias"); + + peekAndMatch(","); + } else if (peekAndMatch("Principal")) { + if (principals == null) { + principals = new LinkedList<>(); + } + + String principalClass; + String principalName; + + if (peek("\"")) { + // both the principalClass and principalName + // will be replaced later + principalClass = PrincipalEntry.REPLACE_NAME; + principalName = match("principal type"); + } else { + // check for principalClass wildcard + if (peek("*")) { + match("*"); + principalClass = PrincipalEntry.WILDCARD_CLASS; + } else { + principalClass = match("principal type"); + } + + // check for principalName wildcard + if (peek("*")) { + match("*"); + principalName = PrincipalEntry.WILDCARD_NAME; + } else { + principalName = match("quoted string"); + } + + // disallow WILDCARD_CLASS && actual name + if (principalClass.equals(PrincipalEntry.WILDCARD_CLASS) && !principalName.equals(PrincipalEntry.WILDCARD_NAME)) { + throw new ParsingException(st.lineno(), "Cannot specify Principal with a wildcard class without a wildcard name"); + } + } + + try { + principalName = expand(principalName); + + if (principalClass.equals("javax.security.auth.x500.X500Principal") + && !principalName.equals(PrincipalEntry.WILDCARD_NAME)) { + + // 4702543: X500 names with an EmailAddress + // were encoded incorrectly. construct a new + // X500Principal with correct encoding. + + X500Principal p = new X500Principal((new X500Principal(principalName)).toString()); + principalName = p.getName(); + } + + principals.add(new PrincipalEntry(principalClass, principalName)); + } catch (PropertyExpander.ExpandException peee) { + ignoreEntry = true; + } + peekAndMatch(","); + + } else { + throw new ParsingException(st.lineno(), "Expected codeBase or SignedBy or Principal"); + } + } + + if (principals != null) e.principals = principals; + match("{"); + + while (!peek("}")) { + if (peek("Permission")) { + try { + PermissionEntry pe = parsePermissionEntry(); + e.add(pe); + } catch (PropertyExpander.ExpandException peee) { + skipEntry(); // BugId 4219343 + } + match(";"); + } else { + throw new ParsingException(st.lineno(), "Expected permission entry"); + } + } + match("}"); + + try { + if (e.signedBy != null) e.signedBy = expand(e.signedBy); + if (e.codeBase != null) { + e.codeBase = expand(e.codeBase, true).replace(File.separatorChar, '/'); + } + } catch (PropertyExpander.ExpandException peee) { + return null; + } + + return (ignoreEntry) ? null : e; + } + + /** + * parse a Permission entry + */ + private PermissionEntry parsePermissionEntry() throws ParsingException, IOException, PropertyExpander.ExpandException { + PermissionEntry e = new PermissionEntry(); + + // Permission + match("Permission"); + e.permission = match("permission type"); + + if (peek("\"")) { + // Permission name + e.name = expand(match("quoted string")); + } + + if (!peek(",")) { + return e; + } + match(","); + + if (peek("\"")) { + e.action = expand(match("quoted string")); + if (!peek(",")) { + return e; + } + match(","); + } + + if (peekAndMatch("SignedBy")) { + e.signedBy = expand(match("quoted string")); + } + return e; + } + + /** + * parse a domain entry + */ + private DomainEntry parseDomainEntry() throws ParsingException, IOException { + DomainEntry domainEntry; + String name; + Map properties = new HashMap<>(); + + match("domain"); + name = match("domain name"); + + while (!peek("{")) { + // get the domain properties + properties = parseProperties("{"); + } + match("{"); + domainEntry = new DomainEntry(name, properties); + + while (!peek("}")) { + + match("keystore"); + name = match("keystore name"); + // get the keystore properties + if (!peek("}")) { + properties = parseProperties(";"); + } + match(";"); + domainEntry.add(new KeyStoreEntry(name, properties)); + } + match("}"); + + return domainEntry; + } + + /* + * Return a collection of domain properties or keystore properties. + */ + private Map parseProperties(String terminator) throws ParsingException, IOException { + + Map properties = new HashMap<>(); + String key; + String value; + while (!peek(terminator)) { + key = match("property name"); + match("="); + + try { + value = expand(match("quoted string")); + } catch (PropertyExpander.ExpandException peee) { + throw new IOException(peee.getLocalizedMessage()); + } + properties.put(key.toLowerCase(Locale.ENGLISH), value); + } + + return properties; + } + + private boolean peekAndMatch(String expect) throws ParsingException, IOException { + if (peek(expect)) { + match(expect); + return true; + } else { + return false; + } + } + + private boolean peek(String expect) { + boolean found = false; + + switch (lookahead) { + + case StreamTokenizer.TT_WORD: + if (expect.equalsIgnoreCase(st.sval)) found = true; + break; + case ',': + if (expect.equalsIgnoreCase(",")) found = true; + break; + case '{': + if (expect.equalsIgnoreCase("{")) found = true; + break; + case '}': + if (expect.equalsIgnoreCase("}")) found = true; + break; + case '"': + if (expect.equalsIgnoreCase("\"")) found = true; + break; + case '*': + if (expect.equalsIgnoreCase("*")) found = true; + break; + case ';': + if (expect.equalsIgnoreCase(";")) found = true; + break; + default: + + } + return found; + } + + private String match(String expect) throws ParsingException, IOException { + String value = null; + + switch (lookahead) { + case StreamTokenizer.TT_NUMBER: + throw new ParsingException(st.lineno(), expect); + case StreamTokenizer.TT_EOF: + Object[] source = { expect }; + String msg = "expected [" + expect + "], read [end of file]"; + throw new ParsingException(msg, source); + case StreamTokenizer.TT_WORD: + if (expect.equalsIgnoreCase(st.sval)) { + lookahead = st.nextToken(); + } else if (expect.equalsIgnoreCase("permission type")) { + value = st.sval; + lookahead = st.nextToken(); + } else if (expect.equalsIgnoreCase("principal type")) { + value = st.sval; + lookahead = st.nextToken(); + } else if (expect.equalsIgnoreCase("domain name") + || expect.equalsIgnoreCase("keystore name") + || expect.equalsIgnoreCase("property name")) { + value = st.sval; + lookahead = st.nextToken(); + } else { + throw new ParsingException(st.lineno(), expect, st.sval); + } + break; + case '"': + if (expect.equalsIgnoreCase("quoted string")) { + value = st.sval; + lookahead = st.nextToken(); + } else if (expect.equalsIgnoreCase("permission type")) { + value = st.sval; + lookahead = st.nextToken(); + } else if (expect.equalsIgnoreCase("principal type")) { + value = st.sval; + lookahead = st.nextToken(); + } else { + throw new ParsingException(st.lineno(), expect, st.sval); + } + break; + case ',': + if (expect.equalsIgnoreCase(",")) lookahead = st.nextToken(); + else throw new ParsingException(st.lineno(), expect, ","); + break; + case '{': + if (expect.equalsIgnoreCase("{")) lookahead = st.nextToken(); + else throw new ParsingException(st.lineno(), expect, "{"); + break; + case '}': + if (expect.equalsIgnoreCase("}")) lookahead = st.nextToken(); + else throw new ParsingException(st.lineno(), expect, "}"); + break; + case ';': + if (expect.equalsIgnoreCase(";")) lookahead = st.nextToken(); + else throw new ParsingException(st.lineno(), expect, ";"); + break; + case '*': + if (expect.equalsIgnoreCase("*")) lookahead = st.nextToken(); + else throw new ParsingException(st.lineno(), expect, "*"); + break; + case '=': + if (expect.equalsIgnoreCase("=")) lookahead = st.nextToken(); + else throw new ParsingException(st.lineno(), expect, "="); + break; + default: + throw new ParsingException(st.lineno(), expect, String.valueOf((char) lookahead)); + } + return value; + } + + /** + * skip all tokens for this entry leaving the delimiter ";" + * in the stream. + */ + private void skipEntry() throws ParsingException, IOException { + while (lookahead != ';') { + switch (lookahead) { + case StreamTokenizer.TT_NUMBER: + throw new ParsingException(st.lineno(), ";"); + case StreamTokenizer.TT_EOF: + throw new ParsingException("Expected read end of file"); + default: + lookahead = st.nextToken(); + } + } + } + + /** + * Each grant entry in the policy configuration file is + * represented by a GrantEntry object. + * + *

+ * For example, the entry + *

+     *      grant signedBy "Duke" {
+     *          permission java.io.FilePermission "/tmp", "read,write";
+     *      };
+     *
+     * 
+ * is represented internally + *
+     *
+     * pe = new PermissionEntry("java.io.FilePermission",
+     *                           "/tmp", "read,write");
+     *
+     * ge = new GrantEntry("Duke", null);
+     *
+     * ge.add(pe);
+     *
+     * 
+ * + * @author Roland Schemers + * + * version 1.19, 05/21/98 + */ + + public static class GrantEntry { + + public String signedBy; + public String codeBase; + public LinkedList principals; + public Vector permissionEntries; + + public GrantEntry() { + principals = new LinkedList<>(); + permissionEntries = new Vector<>(); + } + + public GrantEntry(String signedBy, String codeBase) { + this.codeBase = codeBase; + this.signedBy = signedBy; + principals = new LinkedList<>(); + permissionEntries = new Vector<>(); + } + + public void add(PermissionEntry pe) { + permissionEntries.addElement(pe); + } + + public boolean remove(PrincipalEntry pe) { + return principals.remove(pe); + } + + public boolean remove(PermissionEntry pe) { + return permissionEntries.removeElement(pe); + } + + public boolean contains(PrincipalEntry pe) { + return principals.contains(pe); + } + + public boolean contains(PermissionEntry pe) { + return permissionEntries.contains(pe); + } + + /** + * Enumerate all the permission entries in this GrantEntry. + */ + public Enumeration permissionElements() { + return permissionEntries.elements(); + } + + public void write(PrintWriter out) { + out.print("grant"); + if (signedBy != null) { + out.print(" signedBy \""); + out.print(signedBy); + out.print('"'); + if (codeBase != null) out.print(", "); + } + if (codeBase != null) { + out.print(" codeBase \""); + out.print(codeBase); + out.print('"'); + if (principals != null && principals.size() > 0) out.print(",\n"); + } + if (principals != null && principals.size() > 0) { + Iterator pli = principals.iterator(); + while (pli.hasNext()) { + out.print(" "); + PrincipalEntry pe = pli.next(); + pe.write(out); + if (pli.hasNext()) out.print(",\n"); + } + } + out.println(" {"); + for (PermissionEntry pe : permissionEntries) { + out.write(" "); + pe.write(out); + } + out.println("};"); + } + + public Object clone() { + GrantEntry ge = new GrantEntry(); + ge.codeBase = this.codeBase; + ge.signedBy = this.signedBy; + ge.principals = new LinkedList<>(this.principals); + ge.permissionEntries = new Vector<>(this.permissionEntries); + return ge; + } + } + + /** + * Principal info (class and name) in a grant entry + */ + public static class PrincipalEntry implements Principal { + + public static final String WILDCARD_CLASS = "WILDCARD_PRINCIPAL_CLASS"; + public static final String WILDCARD_NAME = "WILDCARD_PRINCIPAL_NAME"; + public static final String REPLACE_NAME = "PolicyParser.REPLACE_NAME"; + + String principalClass; + String principalName; + + /** + * A PrincipalEntry consists of the Principal class and Principal name. + * + * @param principalClass the Principal class + * @param principalName the Principal name + * @throws NullPointerException if principalClass or principalName + * are null + */ + public PrincipalEntry(String principalClass, String principalName) { + if (principalClass == null || principalName == null) throw new NullPointerException("principalClass or principalName is null"); + this.principalClass = principalClass; + this.principalName = principalName; + } + + boolean isWildcardName() { + return principalName.equals(WILDCARD_NAME); + } + + boolean isWildcardClass() { + return principalClass.equals(WILDCARD_CLASS); + } + + boolean isReplaceName() { + return principalClass.equals(REPLACE_NAME); + } + + public String getPrincipalClass() { + return principalClass; + } + + public String getPrincipalName() { + return principalName; + } + + public String getDisplayClass() { + if (isWildcardClass()) { + return "*"; + } else if (isReplaceName()) { + return ""; + } else return principalClass; + } + + public String getDisplayName() { + return getDisplayName(false); + } + + public String getDisplayName(boolean addQuote) { + if (isWildcardName()) { + return "*"; + } else { + if (addQuote) return "\"" + principalName + "\""; + else return principalName; + } + } + + @Override + public String getName() { + return principalName; + } + + @Override + public String toString() { + if (!isReplaceName()) { + return getDisplayClass() + "/" + getDisplayName(); + } else { + return getDisplayName(); + } + } + + /** + * Test for equality between the specified object and this object. + * Two PrincipalEntries are equal if their class and name values + * are equal. + * + * @param obj the object to test for equality with this object + * @return true if the objects are equal, false otherwise + */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof PrincipalEntry that)) return false; + + return (principalClass.equals(that.principalClass) && principalName.equals(that.principalName)); + } + + /** + * Return a hashcode for this PrincipalEntry. + * + * @return a hashcode for this PrincipalEntry + */ + @Override + public int hashCode() { + return principalClass.hashCode(); + } + + public void write(PrintWriter out) { + out.print("principal " + getDisplayClass() + " " + getDisplayName(true)); + } + } + + /** + * Each permission entry in the policy configuration file is + * represented by a + * PermissionEntry object. + * + *

+ * For example, the entry + *

+     *          permission java.io.FilePermission "/tmp", "read,write";
+     * 
+ * is represented internally + *
+     *
+     * pe = new PermissionEntry("java.io.FilePermission",
+     *                           "/tmp", "read,write");
+     * 
+ * + * @author Roland Schemers + * + * version 1.19, 05/21/98 + */ + + public static class PermissionEntry { + + public String permission; + public String name; + public String action; + public String signedBy; + + public PermissionEntry() {} + + public PermissionEntry(String permission, String name, String action) { + this.permission = permission; + this.name = name; + this.action = action; + } + + /** + * Calculates a hash code value for the object. Objects + * which are equal will also have the same hashcode. + */ + @Override + public int hashCode() { + int retval = permission.hashCode(); + if (name != null) retval ^= name.hashCode(); + if (action != null) retval ^= action.hashCode(); + return retval; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + + if (!(obj instanceof PermissionEntry that)) return false; + + if (this.permission == null) { + if (that.permission != null) return false; + } else { + if (!this.permission.equals(that.permission)) return false; + } + + if (this.name == null) { + if (that.name != null) return false; + } else { + if (!this.name.equals(that.name)) return false; + } + + if (this.action == null) { + if (that.action != null) return false; + } else { + if (!this.action.equals(that.action)) return false; + } + + if (this.signedBy == null) { + return that.signedBy == null; + } else { + return this.signedBy.equals(that.signedBy); + } + } + + public void write(PrintWriter out) { + out.print("permission "); + out.print(permission); + if (name != null) { + out.print(" \""); + + // ATTENTION: regex with double escaping, + // the normal forms look like: + // $name =~ s/\\/\\\\/g; and + // $name =~ s/\"/\\\"/g; + // and then in a java string, it's escaped again + + out.print(name.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\\\"")); + out.print('"'); + } + if (action != null) { + out.print(", \""); + out.print(action); + out.print('"'); + } + if (signedBy != null) { + out.print(", signedBy \""); + out.print(signedBy); + out.print('"'); + } + out.println(";"); + } + } + + /** + * Each domain entry in the keystore domain configuration file is + * represented by a DomainEntry object. + */ + static class DomainEntry { + private final String name; + private final Map properties; + private final Map entries; + + DomainEntry(String name, Map properties) { + this.name = name; + this.properties = properties; + entries = new HashMap<>(); + } + + String getName() { + return name; + } + + Map getProperties() { + return properties; + } + + Collection getEntries() { + return entries.values(); + } + + void add(KeyStoreEntry entry) throws ParsingException { + String keystoreName = entry.getName(); + if (!entries.containsKey(keystoreName)) { + entries.put(keystoreName, entry); + } else { + Object[] source = { keystoreName }; + String msg = "duplicate keystore name: " + keystoreName; + throw new ParsingException(msg, source); + } + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder("\ndomain ").append(name); + + if (properties != null) { + for (Map.Entry property : properties.entrySet()) { + s.append("\n ").append(property.getKey()).append('=').append(property.getValue()); + } + } + s.append(" {\n"); + + for (KeyStoreEntry entry : entries.values()) { + s.append(entry).append("\n"); + } + s.append("}"); + + return s.toString(); + } + } + + /** + * Each keystore entry in the keystore domain configuration file is + * represented by a KeyStoreEntry object. + */ + + static class KeyStoreEntry { + private final String name; + private final Map properties; + + KeyStoreEntry(String name, Map properties) { + this.name = name; + this.properties = properties; + } + + String getName() { + return name; + } + + Map getProperties() { + return properties; + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder("\n keystore ").append(name); + if (properties != null) { + for (Map.Entry property : properties.entrySet()) { + s.append("\n ").append(property.getKey()).append('=').append(property.getValue()); + } + } + s.append(";"); + + return s.toString(); + } + } + + public static class ParsingException extends GeneralSecurityException { + + @java.io.Serial + private static final long serialVersionUID = -4330692689482574072L; + + @SuppressWarnings("serial") // Not statically typed as Serializable + private Object[] source; + + /** + * Constructs a ParsingException with the specified + * detail message. A detail message is a String that describes + * this particular exception, which may, for example, specify which + * algorithm is not available. + * + * @param msg the detail message. + */ + public ParsingException(String msg) { + super(msg); + } + + public ParsingException(String msg, Object[] source) { + super(msg); + this.source = source; + } + + public ParsingException(int line, String msg) { + super("line " + line + ": " + msg); + source = new Object[] { line, msg }; + } + + public ParsingException(int line, String expect, String actual) { + super("line " + line + ": expected [" + expect + "], found [" + actual + "]"); + source = new Object[] { line, expect, actual }; + } + } +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java new file mode 100644 index 0000000000000..ed19379b697c0 --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Arrays; + +/** + * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/util/PolicyUtil.java + */ +public class PolicyUtil { + + // standard PKCS11 KeyStore type + private static final String P11KEYSTORE = "PKCS11"; + + // reserved word + private static final String NONE = "NONE"; + + /* + * Fast path reading from file urls in order to avoid calling + * FileURLConnection.connect() which can be quite slow the first time + * it is called. We really should clean up FileURLConnection so that + * this is not a problem but in the meantime this fix helps reduce + * start up time noticeably for the new launcher. -- DAC + */ + public static InputStream getInputStream(URL url) throws IOException { + if ("file".equals(url.getProtocol())) { + String path = url.getFile().replace('/', File.separatorChar); + path = ParseUtil.decode(path); + return new FileInputStream(path); + } else { + return url.openStream(); + } + } + + /** + * this is intended for use by the policy parser to + * instantiate a KeyStore from the information in the GUI/policy file + */ + public static KeyStore getKeyStore( + URL policyUrl, // URL of policy file + String keyStoreName, // input: keyStore URL + String keyStoreType, // input: keyStore type + String keyStoreProvider, // input: keyStore provider + String storePassURL // input: keyStore password + ) throws KeyStoreException, IOException, NoSuchProviderException, NoSuchAlgorithmException, java.security.cert.CertificateException { + + if (keyStoreName == null) { + throw new IllegalArgumentException("null KeyStore name"); + } + + char[] keyStorePassword = null; + try { + KeyStore ks; + if (keyStoreType == null) { + keyStoreType = KeyStore.getDefaultType(); + } + + if (P11KEYSTORE.equalsIgnoreCase(keyStoreType) && !NONE.equals(keyStoreName)) { + throw new IllegalArgumentException( + "Invalid value (" + + keyStoreName + + ") for keystore URL. If the keystore type is \"" + + P11KEYSTORE + + "\", the keystore url must be \"" + + NONE + + "\"" + ); + } + + if (keyStoreProvider != null) { + ks = KeyStore.getInstance(keyStoreType, keyStoreProvider); + } else { + ks = KeyStore.getInstance(keyStoreType); + } + + if (storePassURL != null) { + URL passURL; + try { + @SuppressWarnings("deprecation") + var _unused = passURL = new URL(storePassURL); + // absolute URL + } catch (MalformedURLException e) { + // relative URL + if (policyUrl == null) { + throw e; + } + @SuppressWarnings("deprecation") + var _unused = passURL = new URL(policyUrl, storePassURL); + } + + try (InputStream in = passURL.openStream()) { + keyStorePassword = Password.readPassword(in); + } + } + + if (NONE.equals(keyStoreName)) { + ks.load(null, keyStorePassword); + } else { + /* + * location of keystore is specified as absolute URL in policy + * file, or is relative to URL of policy file + */ + URL keyStoreUrl; + try { + @SuppressWarnings("deprecation") + var _unused = keyStoreUrl = new URL(keyStoreName); + // absolute URL + } catch (MalformedURLException e) { + // relative URL + if (policyUrl == null) { + throw e; + } + @SuppressWarnings("deprecation") + var _unused = keyStoreUrl = new URL(policyUrl, keyStoreName); + } + + try (InputStream inStream = new BufferedInputStream(getInputStream(keyStoreUrl))) { + ks.load(inStream, keyStorePassword); + } + } + return ks; + } finally { + if (keyStorePassword != null) { + Arrays.fill(keyStorePassword, ' '); + } + } + } +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java new file mode 100644 index 0000000000000..759822b0ef2b5 --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; + +/** + * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/util/PropertyExpander.java + */ +public class PropertyExpander { + + public static class ExpandException extends GeneralSecurityException { + private static final long serialVersionUID = -1L; + + public ExpandException(String msg) { + super(msg); + } + } + + public static String expand(String value) throws ExpandException { + return expand(value, false); + } + + public static String expand(String value, boolean encodeURL) throws ExpandException { + if (value == null) return null; + + int p = value.indexOf("${"); + + // no special characters + if (p == -1) return value; + + StringBuilder sb = new StringBuilder(value.length()); + int max = value.length(); + int i = 0; // index of last character we copied + + scanner: while (p < max) { + if (p > i) { + // copy in anything before the special stuff + sb.append(value.substring(i, p)); + } + int pe = p + 2; + + // do not expand ${{ ... }} + if (pe < max && value.charAt(pe) == '{') { + pe = value.indexOf("}}", pe); + if (pe == -1 || pe + 2 == max) { + // append remaining chars + sb.append(value.substring(p)); + break scanner; + } else { + // append as normal text + pe++; + sb.append(value.substring(p, pe + 1)); + } + } else { + while ((pe < max) && (value.charAt(pe) != '}')) { + pe++; + } + if (pe == max) { + // no matching '}' found, just add in as normal text + sb.append(value.substring(p, pe)); + break scanner; + } + String prop = value.substring(p + 2, pe); + if (prop.equals("/")) { + sb.append(java.io.File.separatorChar); + } else { + String val = System.getProperty(prop); + if (val != null) { + if (encodeURL) { + // encode 'val' unless it's an absolute URI + // at the beginning of the string buffer + try { + if (sb.length() > 0 || !(new URI(val)).isAbsolute()) { + val = ParseUtil.encodePath(val); + } + } catch (URISyntaxException use) { + val = ParseUtil.encodePath(val); + } + } + sb.append(val); + } else { + throw new ExpandException("unable to expand property " + prop); + } + } + } + i = pe + 1; + p = value.indexOf("${", i); + if (p == -1) { + // no more to expand. copy in any extra + if (i < max) { + sb.append(value.substring(i, max)); + } + // break out of loop + break scanner; + } + } + return sb.toString(); + } +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java new file mode 100644 index 0000000000000..39e8efd87868c --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +import java.lang.reflect.ReflectPermission; +import java.net.NetPermission; +import java.net.SocketPermission; +import java.security.AllPermission; +import java.security.SecurityPermission; + +/** + * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/util/SecurityConstants.java + */ +public final class SecurityConstants { + // Cannot create one of these + private SecurityConstants() {} + + // Commonly used string constants for permission actions used by + // SecurityManager. Declare here for shortcut when checking permissions + // in FilePermission, SocketPermission, and PropertyPermission. + + public static final String FILE_DELETE_ACTION = "delete"; + public static final String FILE_EXECUTE_ACTION = "execute"; + public static final String FILE_READ_ACTION = "read"; + public static final String FILE_WRITE_ACTION = "write"; + public static final String FILE_READLINK_ACTION = "readlink"; + + public static final String SOCKET_RESOLVE_ACTION = "resolve"; + public static final String SOCKET_CONNECT_ACTION = "connect"; + public static final String SOCKET_LISTEN_ACTION = "listen"; + public static final String SOCKET_ACCEPT_ACTION = "accept"; + public static final String SOCKET_CONNECT_ACCEPT_ACTION = "connect,accept"; + + public static final String PROPERTY_RW_ACTION = "read,write"; + public static final String PROPERTY_READ_ACTION = "read"; + public static final String PROPERTY_WRITE_ACTION = "write"; + + // Permission constants used in the various checkPermission() calls in JDK. + + // java.lang.Class, java.lang.SecurityManager, java.lang.System, + // java.net.URLConnection, java.security.AllPermission, java.security.Policy, + // sun.security.provider.PolicyFile + public static final AllPermission ALL_PERMISSION = new AllPermission(); + + // java.net.URL + public static final NetPermission SPECIFY_HANDLER_PERMISSION = new NetPermission("specifyStreamHandler"); + + // java.net.ProxySelector + public static final NetPermission SET_PROXYSELECTOR_PERMISSION = new NetPermission("setProxySelector"); + + // java.net.ProxySelector + public static final NetPermission GET_PROXYSELECTOR_PERMISSION = new NetPermission("getProxySelector"); + + // java.net.CookieHandler + public static final NetPermission SET_COOKIEHANDLER_PERMISSION = new NetPermission("setCookieHandler"); + + // java.net.CookieHandler + public static final NetPermission GET_COOKIEHANDLER_PERMISSION = new NetPermission("getCookieHandler"); + + // java.net.ResponseCache + public static final NetPermission SET_RESPONSECACHE_PERMISSION = new NetPermission("setResponseCache"); + + // java.net.ResponseCache + public static final NetPermission GET_RESPONSECACHE_PERMISSION = new NetPermission("getResponseCache"); + + // java.net.ServerSocket, java.net.Socket + public static final NetPermission SET_SOCKETIMPL_PERMISSION = new NetPermission("setSocketImpl"); + + // java.lang.SecurityManager, sun.applet.AppletPanel + public static final RuntimePermission CREATE_CLASSLOADER_PERMISSION = new RuntimePermission("createClassLoader"); + + // java.lang.SecurityManager + public static final RuntimePermission CHECK_MEMBER_ACCESS_PERMISSION = new RuntimePermission("accessDeclaredMembers"); + + // java.lang.SecurityManager, sun.applet.AppletSecurity + public static final RuntimePermission MODIFY_THREAD_PERMISSION = new RuntimePermission("modifyThread"); + + // java.lang.SecurityManager, sun.applet.AppletSecurity + public static final RuntimePermission MODIFY_THREADGROUP_PERMISSION = new RuntimePermission("modifyThreadGroup"); + + // java.lang.Class + public static final RuntimePermission GET_PD_PERMISSION = new RuntimePermission("getProtectionDomain"); + + // java.lang.Class, java.lang.ClassLoader, java.lang.Thread + public static final RuntimePermission GET_CLASSLOADER_PERMISSION = new RuntimePermission("getClassLoader"); + + // java.lang.Thread + public static final RuntimePermission STOP_THREAD_PERMISSION = new RuntimePermission("stopThread"); + + // java.lang.Thread + public static final RuntimePermission GET_STACK_TRACE_PERMISSION = new RuntimePermission("getStackTrace"); + + // java.lang.Thread + public static final RuntimePermission SUBCLASS_IMPLEMENTATION_PERMISSION = new RuntimePermission("enableContextClassLoaderOverride"); + + // java.security.AccessControlContext + public static final SecurityPermission CREATE_ACC_PERMISSION = new SecurityPermission("createAccessControlContext"); + + // java.security.AccessControlContext + public static final SecurityPermission GET_COMBINER_PERMISSION = new SecurityPermission("getDomainCombiner"); + + // java.security.Policy, java.security.ProtectionDomain + public static final SecurityPermission GET_POLICY_PERMISSION = new SecurityPermission("getPolicy"); + + // java.lang.SecurityManager + public static final SocketPermission LOCAL_LISTEN_PERMISSION = new SocketPermission("localhost:0", SOCKET_LISTEN_ACTION); + + // java.lang.reflect.AccessibleObject + public static final ReflectPermission ACCESS_PERMISSION = new ReflectPermission("suppressAccessChecks"); + + // sun.reflect.ReflectionFactory + public static final RuntimePermission REFLECTION_FACTORY_ACCESS_PERMISSION = new RuntimePermission("reflectionFactoryAccess"); + +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/package-info.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/package-info.java new file mode 100644 index 0000000000000..d182490b8d173 --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * Java Agent Policy + */ +package org.opensearch.secure_sm.policy; diff --git a/libs/agent-sm/agent/build.gradle b/libs/agent-sm/agent/build.gradle index a69dc057f2f9c..e705b8a51c6c0 100644 --- a/libs/agent-sm/agent/build.gradle +++ b/libs/agent-sm/agent/build.gradle @@ -74,3 +74,7 @@ tasks.test { tasks.check { dependsOn test } + +tasks.named('assemble') { + dependsOn prepareAgent +} diff --git a/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SystemExitInterceptor.java b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SystemExitInterceptor.java index 20087500f1df4..95489fdea5f55 100644 --- a/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SystemExitInterceptor.java +++ b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SystemExitInterceptor.java @@ -11,6 +11,7 @@ import org.opensearch.javaagent.bootstrap.AgentPolicy; import java.lang.StackWalker.Option; +import java.security.Policy; import net.bytebuddy.asm.Advice; @@ -30,6 +31,11 @@ public SystemExitInterceptor() {} */ @Advice.OnMethodEnter() public static void intercept(int code) throws Exception { + final Policy policy = AgentPolicy.getPolicy(); + if (policy == null) { + return; /* noop */ + } + final StackWalker walker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE); final Class caller = walker.getCallerClass(); diff --git a/libs/build.gradle b/libs/build.gradle index 9bf359d936178..2ac4e03819f3a 100644 --- a/libs/build.gradle +++ b/libs/build.gradle @@ -41,20 +41,21 @@ subprojects { */ project.afterEvaluate { if (!project.path.equals(':libs:agent-sm:agent')) { - configurations.all { Configuration conf -> - dependencies.matching { it instanceof ProjectDependency }.all { ProjectDependency dep -> - Project depProject = project.project(dep.path) - if (depProject != null - && (false == depProject.path.equals(':libs:opensearch-core') && - false == depProject.path.equals(':libs:opensearch-common')) - && depProject.path.startsWith(':libs')) { - throw new InvalidUserDataException("projects in :libs " - + "may not depend on other projects libs except " - + ":libs:opensearch-core or :libs:opensearch-common but " - + "${project.path} depends on ${depProject.path}") + configurations.all { Configuration conf -> + dependencies.matching { it instanceof ProjectDependency }.all { ProjectDependency dep -> + Project depProject = project.project(dep.path) + if (depProject != null + && (false == depProject.path.equals(':libs:opensearch-core') && + false == depProject.path.equals(':libs:opensearch-common')&& + false == depProject.path.equals(':libs:agent-sm:agent-policy')) + && depProject.path.startsWith(':libs')) { + throw new InvalidUserDataException("projects in :libs " + + "may not depend on other projects libs except " + + ":libs:opensearch-core or :libs:opensearch-common but " + + "${project.path} depends on ${depProject.path}") + } } } - } } } } diff --git a/libs/nio/src/test/java/org/opensearch/nio/SocketChannelContextTests.java b/libs/nio/src/test/java/org/opensearch/nio/SocketChannelContextTests.java index 1e559b597ca89..c20f7bd906af0 100644 --- a/libs/nio/src/test/java/org/opensearch/nio/SocketChannelContextTests.java +++ b/libs/nio/src/test/java/org/opensearch/nio/SocketChannelContextTests.java @@ -125,6 +125,7 @@ public void testSignalWhenPeerClosed() throws IOException { assertTrue(context.closeNow()); } + @AwaitsFix(bugUrl = "https://github.com/opensearch-project/OpenSearch/pull/16731") public void testRegisterInitiatesConnect() throws IOException { InetSocketAddress address = mock(InetSocketAddress.class); boolean isAccepted = randomBoolean(); @@ -205,6 +206,7 @@ public void testConnectFails() throws IOException { assertSame(ioException, exception.get()); } + @AwaitsFix(bugUrl = "https://github.com/opensearch-project/OpenSearch/pull/16731") public void testConnectCanSetSocketOptions() throws IOException { InetSocketAddress address = mock(InetSocketAddress.class); Config.Socket config; diff --git a/libs/secure-sm/build.gradle b/libs/secure-sm/build.gradle index 7a0b06699bf35..9febde423f796 100644 --- a/libs/secure-sm/build.gradle +++ b/libs/secure-sm/build.gradle @@ -31,6 +31,7 @@ apply plugin: 'opensearch.publish' dependencies { // do not add non-test compile dependencies to secure-sm without a good reason to do so + api project(":libs:agent-sm:agent-policy") testImplementation "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}" testImplementation "junit:junit:${versions.junit}" diff --git a/plugins/repository-hdfs/build.gradle b/plugins/repository-hdfs/build.gradle index d3c92ac39f5b4..466abb0e60e02 100644 --- a/plugins/repository-hdfs/build.gradle +++ b/plugins/repository-hdfs/build.gradle @@ -146,7 +146,7 @@ for (String fixtureName : ['hdfsFixture', 'haHdfsFixture', 'secureHdfsFixture', } final List miniHDFSArgs = [] - if (BuildParams.runtimeJavaVersion >= JavaVersion.VERSION_23) { + if (BuildParams.runtimeJavaVersion >= JavaVersion.VERSION_23 && BuildParams.runtimeJavaVersion < JavaVersion.VERSION_24) { miniHDFSArgs.add('-Djava.security.manager=allow') } diff --git a/plugins/repository-hdfs/src/test/resources/org/opensearch/bootstrap/test.policy b/plugins/repository-hdfs/src/test/resources/org/opensearch/bootstrap/test.policy new file mode 100644 index 0000000000000..7899f339e5732 --- /dev/null +++ b/plugins/repository-hdfs/src/test/resources/org/opensearch/bootstrap/test.policy @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +grant { + permission java.net.NetPermission "accessUnixDomainSocket"; + permission java.net.SocketPermission "*", "connect,resolve"; +}; diff --git a/plugins/repository-s3/src/internalClusterTest/resources/org/opensearch/bootstrap/test.policy b/plugins/repository-s3/src/internalClusterTest/resources/org/opensearch/bootstrap/test.policy new file mode 100644 index 0000000000000..7899f339e5732 --- /dev/null +++ b/plugins/repository-s3/src/internalClusterTest/resources/org/opensearch/bootstrap/test.policy @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +grant { + permission java.net.NetPermission "accessUnixDomainSocket"; + permission java.net.SocketPermission "*", "connect,resolve"; +}; diff --git a/server/build.gradle b/server/build.gradle index fd2cac4c7506f..dfcfc5c6df99e 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -71,6 +71,7 @@ dependencies { api project(":libs:opensearch-task-commons") implementation project(':libs:opensearch-arrow-spi') + compileOnly project(":libs:agent-sm:bootstrap") compileOnly project(':libs:opensearch-plugin-classloader') testRuntimeOnly project(':libs:opensearch-plugin-classloader') @@ -378,7 +379,7 @@ tasks.named("licenseHeaders").configure { tasks.test { environment "node.roles.test", "[]" if (BuildParams.runtimeJavaVersion > JavaVersion.VERSION_1_8) { - jvmArgs += ["--add-opens", "java.base/java.nio.file=ALL-UNNAMED"] + jvmArgs += ["--add-opens", "java.base/java.nio.file=ALL-UNNAMED", "-Djdk.attach.allowAttachSelf=true", "-XX:+EnableDynamicAgentLoading" ] } } diff --git a/server/src/main/java/org/opensearch/bootstrap/BootstrapChecks.java b/server/src/main/java/org/opensearch/bootstrap/BootstrapChecks.java index 8285f361ee220..b484c33fda5c9 100644 --- a/server/src/main/java/org/opensearch/bootstrap/BootstrapChecks.java +++ b/server/src/main/java/org/opensearch/bootstrap/BootstrapChecks.java @@ -47,6 +47,7 @@ import org.opensearch.discovery.DiscoveryModule; import org.opensearch.env.Environment; import org.opensearch.index.IndexModule; +import org.opensearch.javaagent.bootstrap.AgentPolicy; import org.opensearch.monitor.jvm.JvmInfo; import org.opensearch.monitor.process.ProcessProbe; import org.opensearch.node.NodeRoleSettings; @@ -57,6 +58,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.AllPermission; +import java.security.Policy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -720,10 +722,10 @@ public final BootstrapCheckResult check(BootstrapContext context) { @SuppressWarnings("removal") boolean isAllPermissionGranted() { - final SecurityManager sm = System.getSecurityManager(); - assert sm != null; + final Policy policy = AgentPolicy.getPolicy(); + assert policy != null; try { - sm.checkPermission(new AllPermission()); + AgentPolicy.checkPermission(new AllPermission()); } catch (final SecurityException e) { return false; } diff --git a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java index 162b9be318cd5..7b011b5828428 100644 --- a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java +++ b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java @@ -48,7 +48,6 @@ import java.io.IOException; import java.nio.file.Path; -import java.security.Permission; import java.security.Security; import java.util.Arrays; import java.util.Locale; @@ -86,19 +85,7 @@ class OpenSearch extends EnvironmentAwareCommand { @SuppressWarnings("removal") public static void main(final String[] args) throws Exception { overrideDnsCachePolicyProperties(); - /* - * We want the JVM to think there is a security manager installed so that if internal policy decisions that would be based on the - * presence of a security manager or lack thereof act as if there is a security manager present (e.g., DNS cache policy). This - * forces such policies to take effect immediately. - */ - System.setSecurityManager(new SecurityManager() { - - @Override - public void checkPermission(Permission perm) { - // grant all permissions so that we can later set the security manager to the one that we want - } - }); LogConfigurator.registerErrorListener(); final OpenSearch opensearch = new OpenSearch(); int status = main(args, opensearch, Terminal.DEFAULT); diff --git a/server/src/main/java/org/opensearch/bootstrap/Security.java b/server/src/main/java/org/opensearch/bootstrap/Security.java index 9c93b0414bdda..2f1de651d0e82 100644 --- a/server/src/main/java/org/opensearch/bootstrap/Security.java +++ b/server/src/main/java/org/opensearch/bootstrap/Security.java @@ -41,9 +41,10 @@ import org.opensearch.common.transport.PortsRange; import org.opensearch.env.Environment; import org.opensearch.http.HttpTransportSettings; +import org.opensearch.javaagent.bootstrap.AgentPolicy; import org.opensearch.plugins.PluginInfo; import org.opensearch.plugins.PluginsService; -import org.opensearch.secure_sm.SecureSM; +import org.opensearch.secure_sm.policy.PolicyFile; import org.opensearch.transport.TcpTransport; import java.io.IOException; @@ -59,7 +60,6 @@ import java.security.NoSuchAlgorithmException; import java.security.Permissions; import java.security.Policy; -import java.security.URIParameter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -144,23 +144,25 @@ static void configure(Environment environment, boolean filterBadDefaults) throws // enable security policy: union of template and environment-based paths, and possibly plugin permissions Map codebases = getCodebaseJarMap(JarHell.parseClassPath()); - Policy.setPolicy( + + // enable security manager + final String[] classesThatCanExit = new String[] { + // SecureSM matches class names as regular expressions so we escape the $ that arises from the nested class name + OpenSearchUncaughtExceptionHandler.PrivilegedHaltAction.class.getName().replace("$", "\\$"), + Command.class.getName() }; + + AgentPolicy.setPolicy( new OpenSearchPolicy( codebases, createPermissions(environment), getPluginPermissions(environment), filterBadDefaults, createRecursiveDataPathPermission(environment) - ) + ), + Set.of() /* trusted hosts */, + classesThatCanExit ); - // enable security manager - final String[] classesThatCanExit = new String[] { - // SecureSM matches class names as regular expressions so we escape the $ that arises from the nested class name - OpenSearchUncaughtExceptionHandler.PrivilegedHaltAction.class.getName().replace("$", "\\$"), - Command.class.getName() }; - System.setSecurityManager(new SecureSM(classesThatCanExit)); - // do some basic tests selfTest(); } @@ -280,14 +282,14 @@ static Policy readPolicy(URL policyFile, Map codebases) { addCodebaseToSystemProperties(propertiesSet, url, property, aliasProperty); } - return Policy.getInstance("JavaPolicy", new URIParameter(policyFile.toURI())); + return new PolicyFile(policyFile); } finally { // clear codebase properties for (String property : propertiesSet) { System.clearProperty(property); } } - } catch (NoSuchAlgorithmException | URISyntaxException e) { + } catch (final RuntimeException e) { throw new IllegalArgumentException("unable to parse policy file `" + policyFile + "`", e); } } diff --git a/server/src/main/java/org/opensearch/common/util/concurrent/ThreadContext.java b/server/src/main/java/org/opensearch/common/util/concurrent/ThreadContext.java index 8c15706adceeb..d680fc04789f8 100644 --- a/server/src/main/java/org/opensearch/common/util/concurrent/ThreadContext.java +++ b/server/src/main/java/org/opensearch/common/util/concurrent/ThreadContext.java @@ -44,14 +44,12 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.common.io.stream.Writeable; import org.opensearch.http.HttpTransportSettings; -import org.opensearch.secure_sm.ThreadContextPermission; import org.opensearch.tasks.Task; import org.opensearch.tasks.TaskThreadContextStatePropagator; import org.opensearch.transport.client.OriginSettingClient; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.Permission; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -114,11 +112,6 @@ public final class ThreadContext implements Writeable { public static final String ACTION_ORIGIN_TRANSIENT_NAME = "action.origin"; // thread context permissions - - private static final Permission ACCESS_SYSTEM_THREAD_CONTEXT_PERMISSION = new ThreadContextPermission("markAsSystemContext"); - private static final Permission STASH_AND_MERGE_THREAD_CONTEXT_PERMISSION = new ThreadContextPermission("stashAndMergeHeaders"); - private static final Permission STASH_WITH_ORIGIN_THREAD_CONTEXT_PERMISSION = new ThreadContextPermission("stashWithOrigin"); - private static final Logger logger = LogManager.getLogger(ThreadContext.class); private static final ThreadContextStruct DEFAULT_CONTEXT = new ThreadContextStruct(); private final Map defaultHeader; @@ -223,10 +216,6 @@ public Writeable captureAsWriteable() { */ @SuppressWarnings("removal") public StoredContext stashWithOrigin(String origin) { - SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - sm.checkPermission(STASH_WITH_ORIGIN_THREAD_CONTEXT_PERMISSION); - } final ThreadContext.StoredContext storedContext = stashContext(); putTransient(ACTION_ORIGIN_TRANSIENT_NAME, origin); return storedContext; @@ -246,10 +235,6 @@ public StoredContext stashWithOrigin(String origin) { */ @SuppressWarnings("removal") public StoredContext stashAndMergeHeaders(Map headers) { - SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - sm.checkPermission(STASH_AND_MERGE_THREAD_CONTEXT_PERMISSION); - } final ThreadContextStruct context = threadLocal.get(); Map newHeader = new HashMap<>(headers); newHeader.putAll(context.requestHeaders); @@ -605,10 +590,6 @@ boolean isDefaultContext() { */ @SuppressWarnings("removal") public void markAsSystemContext() { - SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - sm.checkPermission(ACCESS_SYSTEM_THREAD_CONTEXT_PERMISSION); - } threadLocal.set(threadLocal.get().setSystemContext(propagators)); } diff --git a/server/src/main/resources/org/opensearch/bootstrap/security.policy b/server/src/main/resources/org/opensearch/bootstrap/security.policy index f521ce0011540..fbe0afb3c2a95 100644 --- a/server/src/main/resources/org/opensearch/bootstrap/security.policy +++ b/server/src/main/resources/org/opensearch/bootstrap/security.policy @@ -93,6 +93,30 @@ grant codeBase "${codebase.reactor-core}" { permission java.net.SocketPermission "*", "connect,resolve"; }; +grant codeBase "${codebase.opensearch-cli}" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +grant codeBase "${codebase.opensearch-core}" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +grant codeBase "${codebase.jackson-core}" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +grant codeBase "${codebase.opensearch-common}" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +grant codeBase "${codebase.opensearch-x-content}" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +grant codeBase "${codebase.opensearch}" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + //// Everything else: grant { diff --git a/server/src/main/resources/org/opensearch/bootstrap/test-framework.policy b/server/src/main/resources/org/opensearch/bootstrap/test-framework.policy index 78f302e9b23db..5fe1a5b64e6c7 100644 --- a/server/src/main/resources/org/opensearch/bootstrap/test-framework.policy +++ b/server/src/main/resources/org/opensearch/bootstrap/test-framework.policy @@ -101,9 +101,14 @@ grant codeBase "${codebase.junit}" { permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; }; +grant codeBase "${codebase.opensearch-core}" { + // opensearch-nio makes and accepts socket connections + permission java.net.SocketPermission "*", "accept,resolve,connect"; +}; + grant codeBase "${codebase.opensearch-nio}" { // opensearch-nio makes and accepts socket connections - permission java.net.SocketPermission "*", "accept,connect"; + permission java.net.SocketPermission "*", "accept,resolve,connect"; }; grant codeBase "${codebase.opensearch-rest-client}" { @@ -111,16 +116,19 @@ grant codeBase "${codebase.opensearch-rest-client}" { permission java.net.SocketPermission "*", "connect"; // rest client uses system properties which gets the default proxy permission java.net.NetPermission "getProxySelector"; + permission java.net.NetPermission "accessUnixDomainSocket"; }; grant codeBase "${codebase.httpcore5}" { // httpcore makes socket connections for rest tests permission java.net.SocketPermission "*", "connect"; + permission java.net.NetPermission "accessUnixDomainSocket"; }; grant codeBase "${codebase.httpclient5}" { // httpclient5 makes socket connections for rest tests permission java.net.SocketPermission "*", "connect,resolve"; + permission java.net.NetPermission "accessUnixDomainSocket"; }; grant codeBase "${codebase.httpcore-nio}" { diff --git a/server/src/test/java/org/opensearch/ExceptionSerializationTests.java b/server/src/test/java/org/opensearch/ExceptionSerializationTests.java index dd55abb65d19f..f4cc39684c86b 100644 --- a/server/src/test/java/org/opensearch/ExceptionSerializationTests.java +++ b/server/src/test/java/org/opensearch/ExceptionSerializationTests.java @@ -131,6 +131,7 @@ import java.io.EOFException; import java.io.FileNotFoundException; import java.io.IOException; +import java.net.URI; import java.net.URISyntaxException; import java.nio.file.AccessDeniedException; import java.nio.file.AtomicMoveNotSupportedException; @@ -166,7 +167,7 @@ public void testExceptionRegistration() throws ClassNotFoundException, IOExcepti final Set> notRegistered = new HashSet<>(); final Set> hasDedicatedWrite = new HashSet<>(); final Set> registered = new HashSet<>(); - final String path = "/org/opensearch"; + final String path = "org/opensearch"; final Path coreLibStartPath = PathUtils.get(OpenSearchException.class.getProtectionDomain().getCodeSource().getLocation().toURI()); final Path startPath = PathUtils.get(OpenSearchServerException.class.getProtectionDomain().getCodeSource().getLocation().toURI()) .resolve("org") @@ -254,7 +255,8 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx Files.walkFileTree(coreLibStartPath, visitor); // walk the server module start path Files.walkFileTree(startPath, visitor); - final Path testStartPath = PathUtils.get(ExceptionSerializationTests.class.getResource(path).toURI()); + final URI location = ExceptionSerializationTests.class.getProtectionDomain().getCodeSource().getLocation().toURI(); + final Path testStartPath = PathUtils.get(location).resolve(path); Files.walkFileTree(testStartPath, visitor); assertTrue(notRegistered.remove(TestException.class)); assertTrue(notRegistered.remove(UnknownHeaderException.class)); diff --git a/server/src/test/resources/org/opensearch/bootstrap/test.policy b/server/src/test/resources/org/opensearch/bootstrap/test.policy index c2b5a8e9c0a4e..30396afaf2ca4 100644 --- a/server/src/test/resources/org/opensearch/bootstrap/test.policy +++ b/server/src/test/resources/org/opensearch/bootstrap/test.policy @@ -10,4 +10,10 @@ grant { // allow to test Security policy and codebases permission java.util.PropertyPermission "*", "read,write"; permission java.security.SecurityPermission "createPolicy.JavaPolicy"; + permission java.net.NetPermission "accessUnixDomainSocket"; +}; + +grant codeBase "${codebase.framework}" { + permission java.net.NetPermission "accessUnixDomainSocket"; + permission java.net.SocketPermission "*", "accept,connect"; }; diff --git a/test/framework/build.gradle b/test/framework/build.gradle index e5297ca0807a4..47fdfce960936 100644 --- a/test/framework/build.gradle +++ b/test/framework/build.gradle @@ -53,6 +53,9 @@ dependencies { api "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}" api "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}" + compileOnly project(":libs:agent-sm:bootstrap") + compileOnly "com.github.spotbugs:spotbugs-annotations:4.9.0" + annotationProcessor "org.apache.logging.log4j:log4j-core:${versions.log4j}" } @@ -97,9 +100,12 @@ test { systemProperty 'tests.gradle_wire_compat_versions', BuildParams.bwcVersions.wireCompatible.join(',') systemProperty 'tests.gradle_unreleased_versions', BuildParams.bwcVersions.unreleased.join(',') - if (BuildParams.runtimeJavaVersion >= JavaVersion.VERSION_18) { + if (BuildParams.runtimeJavaVersion >= JavaVersion.VERSION_18 && BuildParams.runtimeJavaVersion <= JavaVersion.VERSION_23) { jvmArgs += ["-Djava.security.manager=allow"] } + + dependsOn(project(':libs:agent-sm:agent').prepareAgent) + jvmArgs += ["-javaagent:" + project(':libs:agent-sm:agent').jar.archiveFile.get()] } tasks.register("integTest", Test) { diff --git a/test/framework/src/main/java/org/opensearch/bootstrap/AgentAttach.java b/test/framework/src/main/java/org/opensearch/bootstrap/AgentAttach.java new file mode 100644 index 0000000000000..0a0df6756f21f --- /dev/null +++ b/test/framework/src/main/java/org/opensearch/bootstrap/AgentAttach.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.bootstrap; + +public final class AgentAttach { + public static boolean agentIsAttached() { + try { + Class.forName("org.opensearch.javaagent.Agent", false, ClassLoader.getSystemClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/test/framework/src/main/java/org/opensearch/bootstrap/BootstrapForTesting.java b/test/framework/src/main/java/org/opensearch/bootstrap/BootstrapForTesting.java index 76c7ce0628aac..dc358af93040b 100644 --- a/test/framework/src/main/java/org/opensearch/bootstrap/BootstrapForTesting.java +++ b/test/framework/src/main/java/org/opensearch/bootstrap/BootstrapForTesting.java @@ -46,9 +46,9 @@ import org.opensearch.common.settings.Settings; import org.opensearch.core.common.Strings; import org.opensearch.core.util.FileSystemUtils; +import org.opensearch.javaagent.bootstrap.AgentPolicy; import org.opensearch.mockito.plugin.PriviledgedMockMaker; import org.opensearch.plugins.PluginInfo; -import org.opensearch.secure_sm.SecureSM; import org.junit.Assert; import java.io.InputStream; @@ -168,7 +168,7 @@ public class BootstrapForTesting { final Optional testPolicy = Optional.ofNullable(Bootstrap.class.getResource("test.policy")) .map(policy -> Security.readPolicy(policy, codebases)); final Policy opensearchPolicy = new OpenSearchPolicy(codebases, perms, getPluginPermissions(), true, new Permissions()); - Policy.setPolicy(new Policy() { + AgentPolicy.setPolicy(new Policy() { @Override public boolean implies(ProtectionDomain domain, Permission permission) { // implements union @@ -176,10 +176,13 @@ public boolean implies(ProtectionDomain domain, Permission permission) { || testFramework.implies(domain, permission) || testPolicy.map(policy -> policy.implies(domain, permission)).orElse(false /* no policy */); } - }); + }, getTrustedHosts(), new String[0] /* classes than can exit */); // Create access control context for mocking PriviledgedMockMaker.createAccessControlContext(); - System.setSecurityManager(SecureSM.createTestSecureSM(getTrustedHosts())); + + if (!AgentAttach.agentIsAttached()) { + throw new RuntimeException("the security agent is not attached"); + } Security.selfTest(); // guarantee plugin classes are initialized first, in case they have one-time hacks. diff --git a/test/framework/src/test/resources/org/opensearch/bootstrap/test.policy b/test/framework/src/test/resources/org/opensearch/bootstrap/test.policy new file mode 100644 index 0000000000000..07c9fe160e985 --- /dev/null +++ b/test/framework/src/test/resources/org/opensearch/bootstrap/test.policy @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +grant codeBase "${codebase.opensearch-nio}" { + permission java.net.NetPermission "accessUnixDomainSocket"; +}; + +grant { + permission java.net.NetPermission "accessUnixDomainSocket"; + permission java.net.SocketPermission "*", "accept,connect"; +}; From e6bf0262bc90cdb9e954c43c78173ed866fa4232 Mon Sep 17 00:00:00 2001 From: Gulshan Date: Thu, 27 Mar 2025 19:10:38 +0530 Subject: [PATCH 2/8] Simplify Policy evaluation Actually simply the parser Remove logic involving principal Working code Working Policy file Remove parse utils Qol move away from static classes --- .../secure_sm/policy/GrantNode.java | 52 + .../secure_sm/policy/ParseUtil.java | 616 ------- .../opensearch/secure_sm/policy/Password.java | 173 -- .../secure_sm/policy/PermissionNode.java | 48 + .../secure_sm/policy/PolicyFile.java | 1447 ++--------------- .../secure_sm/policy/PolicyParser.java | 1148 +------------ .../secure_sm/policy/PolicyUtil.java | 170 -- .../secure_sm/policy/PropertyExpander.java | 31 +- .../secure_sm/policy/SecurityConstants.java | 145 -- .../opensearch/secure_sm/policy/Token.java | 21 + .../secure_sm/policy/TokenStream.java | 54 + .../secure_sm/policy/Tokenizer.java | 60 + 12 files changed, 442 insertions(+), 3523 deletions(-) create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantNode.java delete mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java delete mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Password.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionNode.java delete mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java delete mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Token.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/TokenStream.java create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Tokenizer.java diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantNode.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantNode.java new file mode 100644 index 0000000000000..595a6fa4b7f13 --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantNode.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.secure_sm.policy; + +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedList; + +public class GrantNode { + public String codeBase; + private final LinkedList permissionEntries = new LinkedList<>(); + + public void add(PermissionNode entry) { + permissionEntries.add(entry); + } + + public Enumeration permissionElements() { + return Collections.enumeration(permissionEntries); + } + + public void write(PrintWriter out) { + out.print("grant"); + if (codeBase != null) { + out.print(" Codebase \""); + out.print(codeBase); + out.print("\""); + } + out.println(" {"); + for (PermissionNode pe : permissionEntries) { + out.print(" permission "); + out.print(pe.permission); + if (pe.name != null) { + out.print(" \""); + out.print(pe.name); + out.print("\""); + } + if (pe.action != null) { + out.print(", \""); + out.print(pe.action); + out.print("\""); + } + out.println(";"); + } + out.println("};"); + } +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java deleted file mode 100644 index d4477fa13fdcd..0000000000000 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java +++ /dev/null @@ -1,616 +0,0 @@ -/* - * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.secure_sm.policy; - -import java.io.File; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.CoderResult; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; -import java.util.HexFormat; - -/** - * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/net/www/ParseUtil.java - */ -public final class ParseUtil { - - private static final HexFormat HEX_UPPERCASE = HexFormat.of().withUpperCase(); - - private ParseUtil() {} - - /** - * Constructs an encoded version of the specified path string suitable - * for use in the construction of a URL. - * - * A path separator is replaced by a forward slash. The string is UTF8 - * encoded. The % escape sequence is used for characters that are above - * 0x7F or those defined in RFC2396 as reserved or excluded in the path - * component of a URL. - */ - public static String encodePath(String path) { - return encodePath(path, true); - } - - /* - * flag indicates whether path uses platform dependent - * File.separatorChar or not. True indicates path uses platform - * dependent File.separatorChar. - */ - public static String encodePath(String path, boolean flag) { - if (flag && File.separatorChar != '/') { - return encodePath(path, 0, File.separatorChar); - } else { - int index = firstEncodeIndex(path); - if (index > -1) { - return encodePath(path, index, '/'); - } else { - return path; - } - } - } - - private static int firstEncodeIndex(String path) { - int len = path.length(); - for (int i = 0; i < len; i++) { - char c = path.charAt(i); - // Ordering in the following test is performance sensitive, - // and typically paths have most chars in the a-z range, then - // in the symbol range '&'-':' (includes '.', '/' and '0'-'9') - // and more rarely in the A-Z range. - if (c >= 'a' && c <= 'z' || c >= '&' && c <= ':' || c >= 'A' && c <= 'Z') { - continue; - } else if (c > 0x007F || match(c, L_ENCODED, H_ENCODED)) { - return i; - } - } - return -1; - } - - private static String encodePath(String path, int index, char sep) { - char[] pathCC = path.toCharArray(); - char[] retCC = new char[pathCC.length * 2 + 16 - index]; - if (index > 0) { - System.arraycopy(pathCC, 0, retCC, 0, index); - } - int retLen = index; - - for (int i = index; i < pathCC.length; i++) { - char c = pathCC[i]; - if (c == sep) retCC[retLen++] = '/'; - else { - if (c <= 0x007F) { - if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9') { - retCC[retLen++] = c; - } else if (match(c, L_ENCODED, H_ENCODED)) { - retLen = escape(retCC, c, retLen); - } else { - retCC[retLen++] = c; - } - } else if (c > 0x07FF) { - retLen = escape(retCC, (char) (0xE0 | ((c >> 12) & 0x0F)), retLen); - retLen = escape(retCC, (char) (0x80 | ((c >> 6) & 0x3F)), retLen); - retLen = escape(retCC, (char) (0x80 | ((c >> 0) & 0x3F)), retLen); - } else { - retLen = escape(retCC, (char) (0xC0 | ((c >> 6) & 0x1F)), retLen); - retLen = escape(retCC, (char) (0x80 | ((c >> 0) & 0x3F)), retLen); - } - } - // worst case scenario for character [0x7ff-] every single - // character will be encoded into 9 characters. - if (retLen + 9 > retCC.length) { - int newLen = retCC.length * 2 + 16; - if (newLen < 0) { - newLen = Integer.MAX_VALUE; - } - char[] buf = new char[newLen]; - System.arraycopy(retCC, 0, buf, 0, retLen); - retCC = buf; - } - } - return new String(retCC, 0, retLen); - } - - /** - * Appends the URL escape sequence for the specified char to the - * specified character array. - */ - private static int escape(char[] cc, char c, int index) { - cc[index++] = '%'; - cc[index++] = Character.forDigit((c >> 4) & 0xF, 16); - cc[index++] = Character.forDigit(c & 0xF, 16); - return index; - } - - /** - * Un-escape and return the character at position i in string s. - */ - private static byte unescape(String s, int i) { - return (byte) Integer.parseInt(s, i + 1, i + 3, 16); - } - - /** - * Returns a new String constructed from the specified String by replacing - * the URL escape sequences and UTF8 encoding with the characters they - * represent. - */ - public static String decode(String s) { - int n = s.length(); - if ((n == 0) || (s.indexOf('%') < 0)) return s; - - StringBuilder sb = new StringBuilder(n); - ByteBuffer bb = ByteBuffer.allocate(n); - CharBuffer cb = CharBuffer.allocate(n); - CharsetDecoder dec = StandardCharsets.UTF_8.newDecoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT); - - char c = s.charAt(0); - for (int i = 0; i < n;) { - assert c == s.charAt(i); - if (c != '%') { - sb.append(c); - if (++i >= n) break; - c = s.charAt(i); - continue; - } - bb.clear(); - for (;;) { - if (n - i < 2) { - throw new IllegalArgumentException("Malformed escape pair: " + s); - } - - try { - bb.put(unescape(s, i)); - } catch (NumberFormatException | IndexOutOfBoundsException e) { - throw new IllegalArgumentException("Malformed escape pair: " + s); - } - i += 3; - if (i >= n) break; - c = s.charAt(i); - if (c != '%') break; - } - bb.flip(); - cb.clear(); - dec.reset(); - CoderResult cr = dec.decode(bb, cb, true); - if (cr.isError()) throw new IllegalArgumentException("Error decoding percent encoded characters"); - cr = dec.flush(cb); - if (cr.isError()) throw new IllegalArgumentException("Error decoding percent encoded characters"); - sb.append(cb.flip().toString()); - } - - return sb.toString(); - } - - public static URL fileToEncodedURL(File file) throws MalformedURLException { - String path = file.getAbsolutePath(); - path = ParseUtil.encodePath(path); - if (!path.startsWith("/")) { - path = "/" + path; - } - if (!path.endsWith("/") && file.isDirectory()) { - path = path + "/"; - } - @SuppressWarnings("deprecation") - var result = new URL("file", "", path); - return result; - } - - public static java.net.URI toURI(URL url) { - String protocol = url.getProtocol(); - String auth = url.getAuthority(); - String path = url.getPath(); - String query = url.getQuery(); - String ref = url.getRef(); - if (path != null && !(path.startsWith("/"))) path = "/" + path; - - // - // In java.net.URI class, a port number of -1 implies the default - // port number. So get it stripped off before creating URI instance. - // - if (auth != null && auth.endsWith(":-1")) auth = auth.substring(0, auth.length() - 3); - - java.net.URI uri; - try { - uri = createURI(protocol, auth, path, query, ref); - } catch (java.net.URISyntaxException e) { - uri = null; - } - return uri; - } - - // - // createURI() and its auxiliary code are cloned from java.net.URI. - // Most of the code are just copy and paste, except that quote() - // has been modified to avoid double-escape. - // - // Usually it is unacceptable, but we're forced to do it because - // otherwise we need to change public API, namely java.net.URI's - // multi-argument constructors. It turns out that the changes cause - // incompatibilities so can't be done. - // - private static URI createURI(String scheme, String authority, String path, String query, String fragment) throws URISyntaxException { - String s = toString(scheme, null, authority, null, null, -1, path, query, fragment); - checkPath(s, scheme, path); - return new URI(s); - } - - private static String toString( - String scheme, - String opaquePart, - String authority, - String userInfo, - String host, - int port, - String path, - String query, - String fragment - ) { - StringBuilder sb = new StringBuilder(); - if (scheme != null) { - sb.append(scheme); - sb.append(':'); - } - appendSchemeSpecificPart(sb, opaquePart, authority, userInfo, host, port, path, query); - appendFragment(sb, fragment); - return sb.toString(); - } - - private static void appendSchemeSpecificPart( - StringBuilder sb, - String opaquePart, - String authority, - String userInfo, - String host, - int port, - String path, - String query - ) { - if (opaquePart != null) { - /* check if SSP begins with an IPv6 address - * because we must not quote a literal IPv6 address - */ - if (opaquePart.startsWith("//[")) { - int end = opaquePart.indexOf(']'); - if (end != -1 && opaquePart.indexOf(':') != -1) { - String doquote, dontquote; - if (end == opaquePart.length()) { - dontquote = opaquePart; - doquote = ""; - } else { - dontquote = opaquePart.substring(0, end + 1); - doquote = opaquePart.substring(end + 1); - } - sb.append(dontquote); - sb.append(quote(doquote, L_URIC, H_URIC)); - } - } else { - sb.append(quote(opaquePart, L_URIC, H_URIC)); - } - } else { - appendAuthority(sb, authority, userInfo, host, port); - if (path != null) sb.append(quote(path, L_PATH, H_PATH)); - if (query != null) { - sb.append('?'); - sb.append(quote(query, L_URIC, H_URIC)); - } - } - } - - private static void appendAuthority(StringBuilder sb, String authority, String userInfo, String host, int port) { - if (host != null) { - sb.append("//"); - if (userInfo != null) { - sb.append(quote(userInfo, L_USERINFO, H_USERINFO)); - sb.append('@'); - } - boolean needBrackets = ((host.indexOf(':') >= 0) && !host.startsWith("[") && !host.endsWith("]")); - if (needBrackets) sb.append('['); - sb.append(host); - if (needBrackets) sb.append(']'); - if (port != -1) { - sb.append(':'); - sb.append(port); - } - } else if (authority != null) { - sb.append("//"); - if (authority.startsWith("[")) { - int end = authority.indexOf(']'); - if (end != -1 && authority.indexOf(':') != -1) { - String doquote, dontquote; - if (end == authority.length()) { - dontquote = authority; - doquote = ""; - } else { - dontquote = authority.substring(0, end + 1); - doquote = authority.substring(end + 1); - } - sb.append(dontquote); - sb.append(quote(doquote, L_REG_NAME | L_SERVER, H_REG_NAME | H_SERVER)); - } - } else { - sb.append(quote(authority, L_REG_NAME | L_SERVER, H_REG_NAME | H_SERVER)); - } - } - } - - private static void appendFragment(StringBuilder sb, String fragment) { - if (fragment != null) { - sb.append('#'); - sb.append(quote(fragment, L_URIC, H_URIC)); - } - } - - // Quote any characters in s that are not permitted - // by the given mask pair - // - private static String quote(String s, long lowMask, long highMask) { - int n = s.length(); - StringBuilder sb = null; - CharsetEncoder encoder = null; - boolean allowNonASCII = ((lowMask & L_ESCAPED) != 0); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c < '\u0080') { - if (!match(c, lowMask, highMask) && !isEscaped(s, i)) { - if (sb == null) { - sb = new StringBuilder(); - sb.append(s, 0, i); - } - appendEscape(sb, (byte) c); - } else { - if (sb != null) sb.append(c); - } - } else if (allowNonASCII && (Character.isSpaceChar(c) || Character.isISOControl(c))) { - if (encoder == null) { - encoder = StandardCharsets.UTF_8.newEncoder(); - } - if (sb == null) { - sb = new StringBuilder(); - sb.append(s, 0, i); - } - appendEncoded(encoder, sb, c); - } else { - if (sb != null) sb.append(c); - } - } - return (sb == null) ? s : sb.toString(); - } - - // - // To check if the given string has an escaped triplet - // at the given position - // - private static boolean isEscaped(String s, int pos) { - if (s == null || (s.length() <= (pos + 2))) return false; - - return s.charAt(pos) == '%' && match(s.charAt(pos + 1), L_HEX, H_HEX) && match(s.charAt(pos + 2), L_HEX, H_HEX); - } - - private static void appendEncoded(CharsetEncoder encoder, StringBuilder sb, char c) { - ByteBuffer bb = null; - try { - bb = encoder.encode(CharBuffer.wrap("" + c)); - } catch (CharacterCodingException x) { - assert false; - } - while (bb.hasRemaining()) { - int b = bb.get() & 0xff; - if (b >= 0x80) appendEscape(sb, (byte) b); - else sb.append((char) b); - } - } - - private static void appendEscape(StringBuilder sb, byte b) { - sb.append('%'); - HEX_UPPERCASE.toHexDigits(sb, b); - } - - // Tell whether the given character is permitted by the given mask pair - private static boolean match(char c, long lowMask, long highMask) { - if (c < 64) return ((1L << c) & lowMask) != 0; - if (c < 128) return ((1L << (c - 64)) & highMask) != 0; - return false; - } - - // If a scheme is given then the path, if given, must be absolute - // - private static void checkPath(String s, String scheme, String path) throws URISyntaxException { - if (scheme != null) { - if (path != null && !path.isEmpty() && path.charAt(0) != '/') throw new URISyntaxException(s, "Relative path in absolute URI"); - } - } - - // -- Character classes for parsing -- - - // To save startup time, we manually calculate the low-/highMask constants. - // For reference, the following methods were used to calculate the values: - - // Compute a low-order mask for the characters - // between first and last, inclusive - // private static long lowMask(char first, char last) { - // long m = 0; - // int f = Math.max(Math.min(first, 63), 0); - // int l = Math.max(Math.min(last, 63), 0); - // for (int i = f; i <= l; i++) - // m |= 1L << i; - // return m; - // } - - // Compute the low-order mask for the characters in the given string - // private static long lowMask(String chars) { - // int n = chars.length(); - // long m = 0; - // for (int i = 0; i < n; i++) { - // char c = chars.charAt(i); - // if (c < 64) - // m |= (1L << c); - // } - // return m; - // } - - // Compute a high-order mask for the characters - // between first and last, inclusive - // private static long highMask(char first, char last) { - // long m = 0; - // int f = Math.max(Math.min(first, 127), 64) - 64; - // int l = Math.max(Math.min(last, 127), 64) - 64; - // for (int i = f; i <= l; i++) - // m |= 1L << i; - // return m; - // } - - // Compute the high-order mask for the characters in the given string - // private static long highMask(String chars) { - // int n = chars.length(); - // long m = 0; - // for (int i = 0; i < n; i++) { - // char c = chars.charAt(i); - // if ((c >= 64) && (c < 128)) - // m |= (1L << (c - 64)); - // } - // return m; - // } - - // Character-class masks - - // digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | - // "8" | "9" - private static final long L_DIGIT = 0x3FF000000000000L; // lowMask('0', '9'); - private static final long H_DIGIT = 0L; - - // hex = digit | "A" | "B" | "C" | "D" | "E" | "F" | - // "a" | "b" | "c" | "d" | "e" | "f" - private static final long L_HEX = L_DIGIT; - private static final long H_HEX = 0x7E0000007EL; // highMask('A', 'F') | highMask('a', 'f'); - - // upalpha = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | - // "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | - // "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" - private static final long L_UPALPHA = 0L; - private static final long H_UPALPHA = 0x7FFFFFEL; // highMask('A', 'Z'); - - // lowalpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | - // "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | - // "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" - private static final long L_LOWALPHA = 0L; - private static final long H_LOWALPHA = 0x7FFFFFE00000000L; // highMask('a', 'z'); - - // alpha = lowalpha | upalpha - private static final long L_ALPHA = L_LOWALPHA | L_UPALPHA; - private static final long H_ALPHA = H_LOWALPHA | H_UPALPHA; - - // alphanum = alpha | digit - private static final long L_ALPHANUM = L_DIGIT | L_ALPHA; - private static final long H_ALPHANUM = H_DIGIT | H_ALPHA; - - // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | - // "(" | ")" - private static final long L_MARK = 0x678200000000L; // lowMask("-_.!~*'()"); - private static final long H_MARK = 0x4000000080000000L; // highMask("-_.!~*'()"); - - // unreserved = alphanum | mark - private static final long L_UNRESERVED = L_ALPHANUM | L_MARK; - private static final long H_UNRESERVED = H_ALPHANUM | H_MARK; - - // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | - // "$" | "," | "[" | "]" - // Added per RFC2732: "[", "]" - private static final long L_RESERVED = 0xAC00985000000000L; // lowMask(";/?:@&=+$,[]"); - private static final long H_RESERVED = 0x28000001L; // highMask(";/?:@&=+$,[]"); - - // The zero'th bit is used to indicate that escape pairs and non-US-ASCII - // characters are allowed; this is handled by the scanEscape method below. - private static final long L_ESCAPED = 1L; - private static final long H_ESCAPED = 0L; - - // uric = reserved | unreserved | escaped - private static final long L_URIC = L_RESERVED | L_UNRESERVED | L_ESCAPED; - private static final long H_URIC = H_RESERVED | H_UNRESERVED | H_ESCAPED; - - // pchar = unreserved | escaped | - // ":" | "@" | "&" | "=" | "+" | "$" | "," - private static final long L_PCHAR = L_UNRESERVED | L_ESCAPED | 0x2400185000000000L; // lowMask(":@&=+$,"); - private static final long H_PCHAR = H_UNRESERVED | H_ESCAPED | 0x1L; // highMask(":@&=+$,"); - - // All valid path characters - private static final long L_PATH = L_PCHAR | 0x800800000000000L; // lowMask(";/"); - private static final long H_PATH = H_PCHAR; // highMask(";/") == 0x0L; - - // Dash, for use in domainlabel and toplabel - private static final long L_DASH = 0x200000000000L; // lowMask("-"); - private static final long H_DASH = 0x0L; // highMask("-"); - - // userinfo = *( unreserved | escaped | - // ";" | ":" | "&" | "=" | "+" | "$" | "," ) - private static final long L_USERINFO = L_UNRESERVED | L_ESCAPED | 0x2C00185000000000L; // lowMask(";:&=+$,"); - private static final long H_USERINFO = H_UNRESERVED | H_ESCAPED; // | highMask(";:&=+$,") == 0L; - - // reg_name = 1*( unreserved | escaped | "$" | "," | - // ";" | ":" | "@" | "&" | "=" | "+" ) - private static final long L_REG_NAME = L_UNRESERVED | L_ESCAPED | 0x2C00185000000000L; // lowMask("$,;:@&=+"); - private static final long H_REG_NAME = H_UNRESERVED | H_ESCAPED | 0x1L; // highMask("$,;:@&=+"); - - // All valid characters for server-based authorities - private static final long L_SERVER = L_USERINFO | L_ALPHANUM | L_DASH | 0x400400000000000L; // lowMask(".:@[]"); - private static final long H_SERVER = H_USERINFO | H_ALPHANUM | H_DASH | 0x28000001L; // highMask(".:@[]"); - - // Characters that are encoded in the path component of a URI. - // - // These characters are reserved in the path segment as described in - // RFC2396 section 3.3: - // "=" | ";" | "?" | "/" - // - // These characters are defined as excluded in RFC2396 section 2.4.3 - // and must be escaped if they occur in the data part of a URI: - // "#" | " " | "<" | ">" | "%" | "\"" | "{" | "}" | "|" | "\\" | "^" | - // "[" | "]" | "`" - // - // Also US ASCII control characters 00-1F and 7F. - - // lowMask((char)0, (char)31) | lowMask("=;?/# <>%\"{}|\\^[]`"); - private static final long L_ENCODED = 0xF800802DFFFFFFFFL; - - // highMask((char)0x7F, (char)0x7F) | highMask("=;?/# <>%\"{}|\\^[]`"); - private static final long H_ENCODED = 0xB800000178000000L; - -} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Password.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Password.java deleted file mode 100644 index ffe5f734fa0ea..0000000000000 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Password.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.secure_sm.policy; - -import java.io.ByteArrayInputStream; -import java.io.Console; -import java.io.IOException; -import java.io.InputStream; -import java.io.PushbackInputStream; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.CodingErrorAction; -import java.util.Arrays; - -/** - * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/util/Password.java - */ -public class Password { - /** Reads user password from given input stream. */ - public static char[] readPassword(InputStream in) throws IOException { - return readPassword(in, false); - } - - /** Reads user password from given input stream. - * @param isEchoOn true if the password should be echoed on the screen - */ - @SuppressWarnings("fallthrough") - public static char[] readPassword(InputStream in, boolean isEchoOn) throws IOException { - - char[] consoleEntered = null; - byte[] consoleBytes = null; - - try { - // Use the new java.io.Console class - Console con = null; - if (!isEchoOn && in == System.in && ((con = System.console()) != null)) { - consoleEntered = con.readPassword(); - // readPassword returns "" if you just print ENTER, - // to be compatible with old Password class, change to null - if (consoleEntered != null && consoleEntered.length == 0) { - return null; - } - consoleBytes = convertToBytes(consoleEntered); - in = new ByteArrayInputStream(consoleBytes); - } - - // Rest of the lines still necessary for KeyStoreLoginModule - // and when there is no console. - - char[] lineBuffer; - char[] buf; - int i; - - buf = lineBuffer = new char[128]; - - int room = buf.length; - int offset = 0; - int c; - - boolean done = false; - while (!done) { - switch (c = in.read()) { - case -1: - case '\n': - done = true; - break; - - case '\r': - int c2 = in.read(); - if ((c2 != '\n') && (c2 != -1)) { - if (!(in instanceof PushbackInputStream)) { - in = new PushbackInputStream(in); - } - ((PushbackInputStream) in).unread(c2); - } else { - done = true; - break; - } - /* fall through */ - default: - if (--room < 0) { - buf = new char[offset + 128]; - room = buf.length - offset - 1; - System.arraycopy(lineBuffer, 0, buf, 0, offset); - Arrays.fill(lineBuffer, ' '); - lineBuffer = buf; - } - buf[offset++] = (char) c; - break; - } - } - - if (offset == 0) { - return null; - } - - char[] ret = new char[offset]; - System.arraycopy(buf, 0, ret, 0, offset); - Arrays.fill(buf, ' '); - - return ret; - } finally { - if (consoleEntered != null) { - Arrays.fill(consoleEntered, ' '); - } - if (consoleBytes != null) { - Arrays.fill(consoleBytes, (byte) 0); - } - } - } - - /** - * Change a password read from Console.readPassword() into - * its original bytes. - * - * @param pass a char[] - * @return its byte[] format, similar to new String(pass).getBytes() - */ - private static byte[] convertToBytes(char[] pass) { - if (enc == null) { - synchronized (Password.class) { - enc = System.console() - .charset() - .newEncoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - } - } - byte[] ba = new byte[(int) (enc.maxBytesPerChar() * pass.length)]; - ByteBuffer bb = ByteBuffer.wrap(ba); - synchronized (enc) { - enc.reset().encode(CharBuffer.wrap(pass), bb, true); - } - if (bb.position() < ba.length) { - ba[bb.position()] = '\n'; - } - return ba; - } - - private static volatile CharsetEncoder enc; -} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionNode.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionNode.java new file mode 100644 index 0000000000000..ca96f91586619 --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionNode.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.secure_sm.policy; + +import java.io.PrintWriter; +import java.util.Objects; + +public class PermissionNode { + public String permission; + public String name; + public String action; + + @Override + public int hashCode() { + return Objects.hash(permission, name, action); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + + return obj instanceof PermissionNode that + && Objects.equals(this.permission, that.permission) + && Objects.equals(this.name, that.name) + && Objects.equals(this.action, that.action); + } + + public void write(PrintWriter out) { + out.print("permission "); + out.print(permission); + if (name != null) { + out.print(" \""); + out.print(name.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\\\"")); + out.print('"'); + } + if (action != null) { + out.print(", \""); + out.print(action); + out.print('"'); + } + out.println(";"); + } +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java index 14b1a8f56375c..057eea60b2cdd 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java @@ -1,28 +1,3 @@ -/* - * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - /* * SPDX-License-Identifier: Apache-2.0 * @@ -33,78 +8,40 @@ package org.opensearch.secure_sm.policy; -import javax.security.auth.Subject; -import javax.security.auth.x500.X500Principal; - import java.io.File; +import java.io.FileInputStream; import java.io.FilePermission; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.ObjectInputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.NetPermission; import java.net.SocketPermission; -import java.net.URI; import java.net.URL; -import java.security.AllPermission; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.security.CodeSource; -import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.Permission; import java.security.PermissionCollection; import java.security.Permissions; -import java.security.Principal; import java.security.ProtectionDomain; -import java.security.Security; -import java.security.SecurityPermission; -import java.security.UnresolvedPermission; import java.security.cert.Certificate; -import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import java.util.Map; -import java.util.PropertyPermission; +import java.util.Optional; import java.util.Set; -import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/provider/PolicyFile.java - */ @SuppressWarnings("removal") public class PolicyFile extends java.security.Policy { - private static final String SELF = "${{self}}"; - private static final String X500PRINCIPAL = "javax.security.auth.x500.X500Principal"; - private static final String POLICY = "java.security.policy"; - private static final String POLICY_URL = "policy.url."; + public static final SocketPermission LOCAL_LISTEN_PERMISSION = new SocketPermission("localhost:0", "listen"); private static final int DEFAULT_CACHE_SIZE = 1; - - // contains the policy grant entries, PD cache, and alias mapping - // can be updated if refresh() is called private volatile PolicyInfo policyInfo; - - private boolean expandProperties = true; - private boolean allowSystemProperties = true; - private boolean notUtf8 = false; private URL url; - // for use with the reflection API - private static final Class[] PARAMS0 = {}; - private static final Class[] PARAMS1 = { String.class }; - private static final Class[] PARAMS2 = { String.class, String.class }; - /** * When a policy file has a syntax error, the exception code may generate * another permission check and this can cause the policy file to be parsed @@ -115,14 +52,6 @@ public class PolicyFile extends java.security.Policy { */ private static Set badPolicyURLs = Collections.newSetFromMap(new ConcurrentHashMap()); - /** - * Initializes the Policy object and reads the default policy - * configuration file(s) into the Policy object. - */ - public PolicyFile() { - init((URL) null); - } - /** * Initializes the Policy object and reads the default policy * from the specified URL only. @@ -147,177 +76,49 @@ private void init(URL url) { } private void initPolicyFile(final PolicyInfo newInfo, final URL url) { - if (url != null) { - - /** - * If the caller specified a URL via Policy.getInstance, - * we only read from default.policy and that URL. - */ - - if (init(url, newInfo) == false) { - // use static policy if all else fails - initStaticPolicy(newInfo); - } - - } else { - - /** - * Caller did not specify URL via Policy.getInstance. - * Read from URLs listed in the java.security properties file. - */ - - boolean loaded_one = initPolicyFile(POLICY, POLICY_URL, newInfo); - // To maintain strict backward compatibility - // we load the static policy only if POLICY load failed - if (!loaded_one) { - // use static policy if all else fails - initStaticPolicy(newInfo); - } - } - } - - private boolean initPolicyFile(final String propname, final String urlname, final PolicyInfo newInfo) { - boolean loaded_policy = false; - - if (allowSystemProperties) { - String extra_policy = System.getProperty(propname); - if (extra_policy != null) { - boolean overrideAll = false; - if (extra_policy.startsWith("=")) { - overrideAll = true; - extra_policy = extra_policy.substring(1); - } - try { - extra_policy = PropertyExpander.expand(extra_policy); - URL policyURL; - - File policyFile = new File(extra_policy); - if (policyFile.exists()) { - policyURL = ParseUtil.fileToEncodedURL(new File(policyFile.getCanonicalPath())); - } else { - policyURL = newURL(extra_policy); - } - if (init(policyURL, newInfo)) { - loaded_policy = true; - } - } catch (Exception e) {} - if (overrideAll) { - return Boolean.valueOf(loaded_policy); - } - } - } - - int n = 1; - String policy_uri; - - while ((policy_uri = Security.getProperty(urlname + n)) != null) { - try { - URL policy_url = null; - String expanded_uri = PropertyExpander.expand(policy_uri).replace(File.separatorChar, '/'); - - if (policy_uri.startsWith("file:${java.home}/") || policy_uri.startsWith("file:${user.home}/")) { - - // this special case accommodates - // the situation java.home/user.home - // expand to a single slash, resulting in - // a file://foo URI - policy_url = new File(expanded_uri.substring(5)).toURI().toURL(); - } else { - policy_url = new URI(expanded_uri).toURL(); - } - - if (init(policy_url, newInfo)) { - loaded_policy = true; - } - } catch (Exception e) { - // ignore that policy - } - n++; - } - return Boolean.valueOf(loaded_policy); + init(url, newInfo); } /** * Reads a policy configuration into the Policy object using a * Reader object. */ - private boolean init(URL policy, PolicyInfo newInfo) { - - // skip parsing policy file if it has been previously parsed and - // has syntax errors + private void init(URL policy, PolicyInfo newInfo) { if (badPolicyURLs.contains(policy)) { - return false; + return; } - try (InputStreamReader isr = getInputStreamReader(PolicyUtil.getInputStream(policy))) { - - PolicyParser pp = new PolicyParser(expandProperties); - pp.read(isr); + try (InputStreamReader reader = new InputStreamReader(getInputStream(policy), StandardCharsets.UTF_8)) { + PolicyParser policyParser = new PolicyParser(); + policyParser.read(reader); - KeyStore keyStore = null; - try { - keyStore = PolicyUtil.getKeyStore( - policy, - pp.getKeyStoreUrl(), - pp.getKeyStoreType(), - pp.getKeyStoreProvider(), - pp.getStorePassURL() - ); - } catch (Exception e) { - // ignore, treat it like we have no keystore - } + Collections.list(policyParser.grantElements()).forEach(grantNode -> { + try { + addGrantNode(grantNode, newInfo); + } catch (Exception e) { + e.printStackTrace(System.err); + } + }); - Enumeration enum_ = pp.grantElements(); - while (enum_.hasMoreElements()) { - PolicyParser.GrantEntry ge = enum_.nextElement(); - addGrantEntry(ge, keyStore, newInfo); - } - return true; + return; } catch (PolicyParser.ParsingException pe) { - // record bad policy file to avoid later reparsing it badPolicyURLs.add(policy); pe.printStackTrace(System.err); - } catch (Exception e) {} - - return false; - } + } catch (IOException ioe) { + ioe.printStackTrace(System.err); + } - private InputStreamReader getInputStreamReader(InputStream is) { - /* - * Read in policy using UTF-8 by default. - * - * Check non-standard system property to see if the default encoding - * should be used instead. - */ - return (notUtf8) ? new InputStreamReader(is) : new InputStreamReader(is, UTF_8); + return; } - private void initStaticPolicy(final PolicyInfo newInfo) { - PolicyEntry pe = new PolicyEntry(new CodeSource(null, (Certificate[]) null)); - pe.add(SecurityConstants.LOCAL_LISTEN_PERMISSION); - pe.add(new PropertyPermission("java.version", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.vendor", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.vendor.url", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.class.version", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("os.name", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("os.version", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("os.arch", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("file.separator", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("path.separator", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("line.separator", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.specification.version", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.specification.maintenance.version", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.specification.vendor", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.specification.name", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.vm.specification.version", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.vm.specification.vendor", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.vm.specification.name", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.vm.version", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.vm.vendor", SecurityConstants.PROPERTY_READ_ACTION)); - pe.add(new PropertyPermission("java.vm.name", SecurityConstants.PROPERTY_READ_ACTION)); - - // No need to sync because no one has access to newInfo yet - newInfo.policyEntries.add(pe); + public static InputStream getInputStream(URL url) throws IOException { + if ("file".equals(url.getProtocol())) { + String path = url.getFile().replace('/', File.separatorChar); + path = URLDecoder.decode(path, StandardCharsets.UTF_8); + return new FileInputStream(path); + } else { + return url.openStream(); + } } /** @@ -325,167 +126,69 @@ private void initStaticPolicy(final PolicyInfo newInfo) { * * @return null if signedBy alias is not recognized */ - private CodeSource getCodeSource(PolicyParser.GrantEntry ge, KeyStore keyStore, PolicyInfo newInfo) - throws java.net.MalformedURLException { + private CodeSource getCodeSource(GrantNode grantEntry, PolicyInfo newInfo) throws java.net.MalformedURLException { Certificate[] certs = null; - if (ge.signedBy != null) { - certs = getCertificates(keyStore, ge.signedBy, newInfo); - if (certs == null) { - return null; - } - } - URL location; - if (ge.codeBase != null) location = newURL(ge.codeBase); + if (grantEntry.codeBase != null) location = newURL(grantEntry.codeBase); else location = null; - return (canonicalizeCodebase(new CodeSource(location, certs), false)); + return (canonicalizeCodebase(new CodeSource(location, certs))); } - /** - * Add one policy entry to the list. - */ - private void addGrantEntry(PolicyParser.GrantEntry ge, KeyStore keyStore, PolicyInfo newInfo) { + private void addGrantNode(GrantNode grantEntry, PolicyInfo newInfo) throws Exception { - try { - CodeSource codesource = getCodeSource(ge, keyStore, newInfo); - // skip if signedBy alias was unknown... - if (codesource == null) return; + CodeSource codesource = getCodeSource(grantEntry, newInfo); + if (codesource == null) return; - // perform keystore alias principal replacement. - // for example, if alias resolves to X509 certificate, - // replace principal with: - // -- skip if alias is unknown - if (replacePrincipals(ge.principals, keyStore) == false) return; - PolicyEntry entry = new PolicyEntry(codesource, ge.principals); - Enumeration enum_ = ge.permissionElements(); - while (enum_.hasMoreElements()) { - PolicyParser.PermissionEntry pe = enum_.nextElement(); + PolicyEntry entry = new PolicyEntry(codesource); + Enumeration enum_ = grantEntry.permissionElements(); + while (enum_.hasMoreElements()) { + PermissionNode pe = enum_.nextElement(); + try { + // Store the original name before expansion + expandPermissionName(pe); - try { - // perform ${{ ... }} expansions within permission name - expandPermissionName(pe, keyStore); + Optional perm = getInstance(pe.permission, pe.name, pe.action); + perm.ifPresent(entry::add); + } catch (ClassNotFoundException cfne) { + cfne.printStackTrace(System.err); + } + } + newInfo.policyEntries.add(entry); + } - // XXX special case PrivateCredentialPermission-SELF - Permission perm; - if (pe.permission.equals("javax.security.auth.PrivateCredentialPermission") && pe.name.endsWith(" self")) { - pe.name = pe.name.substring(0, pe.name.indexOf("self")) + SELF; - } - // check for self - if (pe.name != null && pe.name.contains(SELF)) { - // Create a "SelfPermission" , it could be an - // an unresolved permission which will be resolved - // when implies is called - // Add it to entry - Certificate[] certs; - if (pe.signedBy != null) { - certs = getCertificates(keyStore, pe.signedBy, newInfo); - } else { - certs = null; - } - perm = new SelfPermission(pe.permission, pe.name, pe.action, certs); - } else { - perm = getInstance(pe.permission, pe.name, pe.action); - } - entry.add(perm); - } catch (ClassNotFoundException cnfe) { - Certificate[] certs; - if (pe.signedBy != null) { - certs = getCertificates(keyStore, pe.signedBy, newInfo); - } else { - certs = null; - } + private void expandPermissionName(PermissionNode pe) { + if (pe.name == null || !pe.name.contains("${{")) { + return; + } - // only add if we had no signer or we had - // a signer and found the keys for it. - if (certs != null || pe.signedBy == null) { - Permission perm = new UnresolvedPermission(pe.permission, pe.name, pe.action, certs); - entry.add(perm); - } - } catch (java.lang.reflect.InvocationTargetException ite) { - ite.printStackTrace(System.err); - } catch (Exception e) { - e.printStackTrace(System.err); - } - } + int startIndex = 0; + int b, e; + StringBuilder sb = new StringBuilder(); + + while ((b = pe.name.indexOf("${{", startIndex)) != -1 && (e = pe.name.indexOf("}}", b)) != -1) { - // No need to sync because no one has access to newInfo yet - newInfo.policyEntries.add(entry); - } catch (Exception e) { - e.printStackTrace(System.err); + sb.append(pe.name, startIndex, b); + String value = pe.name.substring(b + 3, e); + + sb.append("${{").append(value).append("}}"); + + startIndex = e + 2; } - } - /** - * Returns a new Permission object of the given Type. The Permission is - * created by getting the - * Class object using the Class.forName method, and using - * the reflection API to invoke the (String name, String actions) - * constructor on the - * object. - * - * @param type the type of Permission being created. - * @param name the name of the Permission being created. - * @param actions the actions of the Permission being created. - * - * @exception ClassNotFoundException if the particular Permission - * class could not be found. - * - * @exception IllegalAccessException if the class or initializer is - * not accessible. - * - * @exception InstantiationException if getInstance tries to - * instantiate an abstract class or an interface, or if the - * instantiation fails for some other reason. - * - * @exception NoSuchMethodException if the (String, String) constructor - * is not found. - * - * @exception InvocationTargetException if the underlying Permission - * constructor throws an exception. - * - */ + sb.append(pe.name.substring(startIndex)); + pe.name = sb.toString(); + } - private static final Permission getInstance(String type, String name, String actions) throws ClassNotFoundException, - InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + private static final Optional getInstance(String type, String name, String actions) throws ClassNotFoundException { Class pc = Class.forName(type, false, null); Permission answer = getKnownPermission(pc, name, actions); if (answer != null) { - return answer; - } - if (!Permission.class.isAssignableFrom(pc)) { - // not the right subtype - throw new ClassCastException(type + " is not a Permission"); + return Optional.of(answer); } - if (name == null && actions == null) { - try { - Constructor c = pc.getConstructor(PARAMS0); - return (Permission) c.newInstance(new Object[] {}); - } catch (NoSuchMethodException ne) { - try { - Constructor c = pc.getConstructor(PARAMS1); - return (Permission) c.newInstance(new Object[] { name }); - } catch (NoSuchMethodException ne1) { - Constructor c = pc.getConstructor(PARAMS2); - return (Permission) c.newInstance(new Object[] { name, actions }); - } - } - } else { - if (name != null && actions == null) { - try { - Constructor c = pc.getConstructor(PARAMS1); - return (Permission) c.newInstance(new Object[] { name }); - } catch (NoSuchMethodException ne) { - Constructor c = pc.getConstructor(PARAMS2); - return (Permission) c.newInstance(new Object[] { name, actions }); - } - } else { - Constructor c = pc.getConstructor(PARAMS2); - return (Permission) c.newInstance(new Object[] { name, actions }); - } - } + return Optional.empty(); } /** @@ -498,78 +201,8 @@ private static Permission getKnownPermission(Class claz, String name, String return new FilePermission(name, actions); } else if (claz.equals(SocketPermission.class)) { return new SocketPermission(name, actions); - } else if (claz.equals(RuntimePermission.class)) { - return new RuntimePermission(name, actions); - } else if (claz.equals(PropertyPermission.class)) { - return new PropertyPermission(name, actions); } else if (claz.equals(NetPermission.class)) { return new NetPermission(name, actions); - } else if (claz.equals(AllPermission.class)) { - return SecurityConstants.ALL_PERMISSION; - } else if (claz.equals(SecurityPermission.class)) { - return new SecurityPermission(name, actions); - } else { - return null; - } - } - - /** - * Creates one of the well-known principals in the java.base module - * directly instead of via reflection. Keep list short to not penalize - * principals from other modules. - */ - private static Principal getKnownPrincipal(Class claz, String name) { - if (claz.equals(X500Principal.class)) { - return new X500Principal(name); - } else { - return null; - } - } - - /** - * Fetch all certs associated with this alias. - */ - private Certificate[] getCertificates(KeyStore keyStore, String aliases, PolicyInfo newInfo) { - - List vcerts = null; - - StringTokenizer st = new StringTokenizer(aliases, ","); - int n = 0; - - while (st.hasMoreTokens()) { - String alias = st.nextToken().trim(); - n++; - Certificate cert = null; - // See if this alias's cert has already been cached - synchronized (newInfo.aliasMapping) { - cert = (Certificate) newInfo.aliasMapping.get(alias); - - if (cert == null && keyStore != null) { - - try { - cert = keyStore.getCertificate(alias); - } catch (KeyStoreException kse) { - // never happens, because keystore has already been loaded - // when we call this - } - if (cert != null) { - newInfo.aliasMapping.put(alias, cert); - newInfo.aliasMapping.put(cert, alias); - } - } - } - - if (cert != null) { - if (vcerts == null) vcerts = new ArrayList<>(); - vcerts.add(cert); - } - } - - // make sure n == vcerts.size, since we are doing a logical *and* - if (vcerts != null && n == vcerts.size()) { - Certificate[] certs = new Certificate[vcerts.size()]; - vcerts.toArray(certs); - return certs; } else { return null; } @@ -583,19 +216,6 @@ public void refresh() { init(url); } - /** - * Evaluates the global policy for the permissions granted to - * the ProtectionDomain and tests whether the permission is - * granted. - * - * @param pd the ProtectionDomain to test - * @param p the Permission object to be tested for implication. - * - * @return true if "permission" is a proper subset of a permission - * granted to this ProtectionDomain. - * - * @see java.security.ProtectionDomain - */ @Override public boolean implies(ProtectionDomain pd, Permission p) { PermissionCollection pc = getPermissions(pd); @@ -607,40 +227,13 @@ public boolean implies(ProtectionDomain pd, Permission p) { return pc.implies(p); } - /** - * Examines this Policy and returns the permissions granted - * to the specified ProtectionDomain. This includes - * the permissions currently associated with the domain as well - * as the policy permissions granted to the domain's - * CodeSource, ClassLoader, and Principals. - * - *

Note that this Policy implementation has - * special handling for PrivateCredentialPermissions. - * When this method encounters a PrivateCredentialPermission - * which specifies "self" as the Principal class and name, - * it does not add that Permission to the returned - * PermissionCollection. Instead, it builds - * a new PrivateCredentialPermission - * for each Principal associated with the provided - * Subject. Each new PrivateCredentialPermission - * contains the same Credential class as specified in the - * originally granted permission, as well as the Class and name - * for the respective Principal. - * - * @param domain the Permissions granted to this - * ProtectionDomain are returned. - * - * @return the Permissions granted to the provided - * ProtectionDomain. - */ @Override public PermissionCollection getPermissions(ProtectionDomain domain) { Permissions perms = new Permissions(); if (domain == null) return perms; - // first get policy perms - getPermissions(perms, domain); + getPermissionsForProtectionDomain(perms, domain); // add static perms // - adding static perms after policy perms is necessary @@ -658,636 +251,99 @@ public PermissionCollection getPermissions(ProtectionDomain domain) { return perms; } - /** - * Examines this Policy and creates a PermissionCollection object with - * the set of permissions for the specified CodeSource. - * - * @param codesource the CodeSource associated with the caller. - * This encapsulates the original location of the code (where the code - * came from) and the public key(s) of its signer. - * - * @return the set of permissions according to the policy. - */ @Override public PermissionCollection getPermissions(CodeSource codesource) { - return getPermissions(new Permissions(), codesource); - } - - /** - * Examines the global policy and returns the provided Permissions - * object with additional permissions granted to the specified - * ProtectionDomain. - * - * @param perms the Permissions to populate - * @param pd the ProtectionDomain associated with the caller. - * - * @return the set of Permissions according to the policy. - */ - private PermissionCollection getPermissions(Permissions perms, ProtectionDomain pd) { - final CodeSource cs = pd.getCodeSource(); - if (cs == null) return perms; - - CodeSource canonCodeSource = canonicalizeCodebase(cs, true); - return getPermissions(perms, canonCodeSource, pd.getPrincipals()); - } - - /** - * Examines the global policy and returns the provided Permissions - * object with additional permissions granted to the specified - * CodeSource. - * - * @param perms the permissions to populate - * @param cs the codesource associated with the caller. - * This encapsulates the original location of the code (where the code - * came from) and the public key(s) of its signer. - * - * @return the set of permissions according to the policy. - */ - private PermissionCollection getPermissions(Permissions perms, final CodeSource cs) { - - if (cs == null) return perms; - - CodeSource canonCodeSource = canonicalizeCodebase(cs, true); - return getPermissions(perms, canonCodeSource, null); - } - - private Permissions getPermissions(Permissions perms, final CodeSource cs, Principal[] principals) { - for (PolicyEntry entry : policyInfo.policyEntries) { - addPermissions(perms, cs, principals, entry); + if (codesource == null) { + return new Permissions(); } - return perms; - } - - private void addPermissions(Permissions perms, final CodeSource cs, Principal[] principals, final PolicyEntry entry) { - - // check to see if the CodeSource implies - Boolean imp = entry.getCodeSource().implies(cs); - if (!imp.booleanValue()) { - // CodeSource does not imply - return and try next policy entry - return; - } - - // check to see if the Principals imply - - List entryPs = entry.getPrincipals(); - - if (entryPs == null || entryPs.isEmpty()) { - - // policy entry has no principals - - // add perms regardless of principals in current ACC - - addPerms(perms, principals, entry); - return; - - } else if (principals == null || principals.length == 0) { - - // current thread has no principals but this policy entry - // has principals - perms are not added - - return; - } - - // current thread has principals and this policy entry - // has principals. see if policy entry principals match - // principals in current ACC - - for (PolicyParser.PrincipalEntry pppe : entryPs) { - - // Check for wildcards - if (pppe.isWildcardClass()) { - // a wildcard class matches all principals in current ACC - continue; - } - - if (pppe.isWildcardName()) { - // a wildcard name matches any principal with the same class - if (wildcardPrincipalNameImplies(pppe.principalClass, principals)) { - continue; - } - // policy entry principal not in current ACC - - // immediately return and go to next policy entry - return; - } - - Set pSet = new HashSet<>(Arrays.asList(principals)); - Subject subject = new Subject(true, pSet, Collections.EMPTY_SET, Collections.EMPTY_SET); - try { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - Class pClass = Class.forName(pppe.principalClass, false, cl); - Principal p = getKnownPrincipal(pClass, pppe.principalName); - if (p == null) { - if (!Principal.class.isAssignableFrom(pClass)) { - // not the right subtype - throw new ClassCastException(pppe.principalClass + " is not a Principal"); - } - - Constructor c = pClass.getConstructor(PARAMS1); - p = (Principal) c.newInstance(new Object[] { pppe.principalName }); - - } - - // check if the Principal implies the current - // thread's principals - if (!p.implies(subject)) { - // policy principal does not imply the current Subject - - // immediately return and go to next policy entry - return; - } - } catch (Exception e) { - // fall back to default principal comparison. - // see if policy entry principal is in current ACC + Permissions perms = new Permissions(); + CodeSource canonicalCodeSource = canonicalizeCodebase(codesource); - if (!pppe.implies(subject)) { - // policy entry principal not in current ACC - - // immediately return and go to next policy entry - return; + for (PolicyEntry entry : policyInfo.policyEntries) { + if (entry.getCodeSource().implies(canonicalCodeSource)) { + for (Permission permission : entry.permissions) { + perms.add(permission); } } - - // either the principal information matched, - // or the Principal.implies succeeded. - // continue loop and test the next policy principal - } - - // all policy entry principals were found in the current ACC - - // grant the policy permissions - - addPerms(perms, principals, entry); - } - - /** - * Returns true if the array of principals contains at least one - * principal of the specified class. - */ - private static boolean wildcardPrincipalNameImplies(String principalClass, Principal[] principals) { - for (Principal p : principals) { - if (principalClass.equals(p.getClass().getName())) { - return true; - } } - return false; - } - private void addPerms(Permissions perms, Principal[] accPs, PolicyEntry entry) { - for (int i = 0; i < entry.permissions.size(); i++) { - Permission p = entry.permissions.get(i); - - if (p instanceof SelfPermission) { - // handle "SELF" permissions - expandSelf((SelfPermission) p, entry.getPrincipals(), accPs, perms); - } else { - perms.add(p); - } - } + return perms; } - /** - * @param sp the SelfPermission that needs to be expanded. - * - * @param entryPs list of principals for the Policy entry. - * - * @param pdp Principal array from the current ProtectionDomain. - * - * @param perms the PermissionCollection where the individual - * Permissions will be added after expansion. - */ - - private void expandSelf(SelfPermission sp, List entryPs, Principal[] pdp, Permissions perms) { - - if (entryPs == null || entryPs.isEmpty()) { - return; - } - int startIndex = 0; - int v; - StringBuilder sb = new StringBuilder(); - while ((v = sp.getSelfName().indexOf(SELF, startIndex)) != -1) { - - // add non-SELF string - sb.append(sp.getSelfName().substring(startIndex, v)); + private PermissionCollection getPermissionsForProtectionDomain(Permissions perms, ProtectionDomain pd) { + final CodeSource cs = pd.getCodeSource(); + if (cs == null) return perms; - // expand SELF - Iterator pli = entryPs.iterator(); - while (pli.hasNext()) { - PolicyParser.PrincipalEntry pppe = pli.next(); - String[][] principalInfo = getPrincipalInfo(pppe, pdp); - for (int i = 0; i < principalInfo.length; i++) { - if (i != 0) { - sb.append(", "); - } - sb.append(principalInfo[i][0] + " " + "\"" + principalInfo[i][1] + "\""); - } - if (pli.hasNext()) { - sb.append(", "); + for (PolicyEntry entry : policyInfo.policyEntries) { + if (entry.getCodeSource().implies(cs)) { + for (Permission permission : entry.permissions) { + perms.add(permission); } } - startIndex = v + SELF.length(); } - // add remaining string (might be the entire string) - sb.append(sp.getSelfName().substring(startIndex)); - - try { - // first try to instantiate the permission - perms.add(getInstance(sp.getSelfType(), sb.toString(), sp.getSelfActions())); - } catch (ClassNotFoundException cnfe) { - // ok, the permission is not in the bootclasspath. - // before we add an UnresolvedPermission, check to see - // whether this perm already belongs to the collection. - // if so, use that perm's ClassLoader to create a new - // one. - Class pc = null; - synchronized (perms) { - Enumeration e = perms.elements(); - while (e.hasMoreElements()) { - Permission pElement = e.nextElement(); - if (pElement.getClass().getName().equals(sp.getSelfType())) { - pc = pElement.getClass(); - break; - } - } - } - if (pc == null) { - // create an UnresolvedPermission - perms.add(new UnresolvedPermission(sp.getSelfType(), sb.toString(), sp.getSelfActions(), sp.getCerts())); - } else { - try { - // we found an instantiated permission. - // use its class loader to instantiate a new permission. - Constructor c; - // name parameter can not be null - if (sp.getSelfActions() == null) { - try { - c = pc.getConstructor(PARAMS1); - perms.add((Permission) c.newInstance(new Object[] { sb.toString() })); - } catch (NoSuchMethodException ne) { - c = pc.getConstructor(PARAMS2); - perms.add((Permission) c.newInstance(new Object[] { sb.toString(), sp.getSelfActions() })); - } - } else { - c = pc.getConstructor(PARAMS2); - perms.add((Permission) c.newInstance(new Object[] { sb.toString(), sp.getSelfActions() })); - } - } catch (Exception nme) {} - } - } catch (Exception e) {} - } - - /** - * return the principal class/name pair in the 2D array. - * array[x][y]: x corresponds to the array length. - * if (y == 0), it's the principal class. - * if (y == 1), it's the principal name. - */ - private String[][] getPrincipalInfo(PolicyParser.PrincipalEntry pe, Principal[] pdp) { - - // there are 3 possibilities: - // 1) the entry's Principal class and name are not wildcarded - // 2) the entry's Principal name is wildcarded only - // 3) the entry's Principal class and name are wildcarded - - if (!pe.isWildcardClass() && !pe.isWildcardName()) { - - // build an info array for the principal - // from the Policy entry - String[][] info = new String[1][2]; - info[0][0] = pe.principalClass; - info[0][1] = pe.principalName; - return info; - } else if (!pe.isWildcardClass() && pe.isWildcardName()) { - - // build an info array for every principal - // in the current domain which has a principal class - // that is equal to policy entry principal class name - List plist = new ArrayList<>(); - for (int i = 0; i < pdp.length; i++) { - if (pe.principalClass.equals(pdp[i].getClass().getName())) plist.add(pdp[i]); - } - String[][] info = new String[plist.size()][2]; - int i = 0; - for (Principal p : plist) { - info[i][0] = p.getClass().getName(); - info[i][1] = p.getName(); - i++; - } - return info; - - } else { - - // build an info array for every - // one of the current Domain's principals - - String[][] info = new String[pdp.length][2]; - - for (int i = 0; i < pdp.length; i++) { - info[i][0] = pdp[i].getClass().getName(); - info[i][1] = pdp[i].getName(); - } - return info; - } + return perms; } - /* - * Returns the signer certificates from the list of certificates - * associated with the given code source. - * - * The signer certificates are those certificates that were used - * to verify signed code originating from the codesource location. - * - * This method assumes that in the given code source, each signer - * certificate is followed by its supporting certificate chain - * (which may be empty), and that the signer certificate and its - * supporting certificate chain are ordered bottom-to-top - * (i.e., with the signer certificate first and the (root) certificate - * authority last). - */ - protected Certificate[] getSignerCertificates(CodeSource cs) { - Certificate[] certs = null; - if ((certs = cs.getCertificates()) == null) return null; - for (int i = 0; i < certs.length; i++) { - if (!(certs[i] instanceof X509Certificate)) return cs.getCertificates(); + private CodeSource canonicalizeCodebase(CodeSource cs) { + URL location = cs.getLocation(); + if (location == null) { + return cs; } - // Do we have to do anything? - int i = 0; - int count = 0; - while (i < certs.length) { - count++; - while (((i + 1) < certs.length) - && ((X509Certificate) certs[i]).getIssuerX500Principal() - .equals(((X509Certificate) certs[i + 1]).getSubjectX500Principal())) { - i++; - } - i++; - } - if (count == certs.length) - // Done - return certs; - - List userCertList = new ArrayList<>(); - i = 0; - while (i < certs.length) { - userCertList.add(certs[i]); - while (((i + 1) < certs.length) - && ((X509Certificate) certs[i]).getIssuerX500Principal() - .equals(((X509Certificate) certs[i + 1]).getSubjectX500Principal())) { - i++; - } - i++; + try { + URL canonicalUrl = canonicalizeUrl(location); + return new CodeSource(canonicalUrl, cs.getCertificates()); + } catch (IOException e) { + // Log the exception or handle it as appropriate + return cs; } - Certificate[] userCerts = new Certificate[userCertList.size()]; - userCertList.toArray(userCerts); - return userCerts; } - private CodeSource canonicalizeCodebase(CodeSource cs, boolean extractSignerCerts) { - - String path = null; - - CodeSource canonCs = cs; - URL u = cs.getLocation(); - if (u != null) { - if (u.getProtocol().equals("jar")) { - // unwrap url embedded inside jar url - String spec = u.getFile(); - int separator = spec.indexOf("!/"); - if (separator != -1) { - try { - u = newURL(spec.substring(0, separator)); - } catch (MalformedURLException e) { - // Fail silently. In this case, url stays what - // it was above - } - } - } - if (u.getProtocol().equals("file")) { - boolean isLocalFile = false; - String host = u.getHost(); - isLocalFile = (host == null || host.isEmpty() || host.equals("~") || host.equalsIgnoreCase("localhost")); + @SuppressWarnings("deprecation") + private URL canonicalizeUrl(URL url) throws IOException { + String protocol = url.getProtocol(); - if (isLocalFile) { - path = u.getFile().replace('/', File.separatorChar); - path = ParseUtil.decode(path); + if ("jar".equals(protocol)) { + String spec = url.getFile(); + int separator = spec.indexOf("!/"); + if (separator != -1) { + try { + url = new URL(spec.substring(0, separator)); + } catch (MalformedURLException e) { + // If unwrapping fails, keep the original URL } } } - if (path != null) { - try { - URL csUrl = null; - path = canonPath(path); - csUrl = ParseUtil.fileToEncodedURL(new File(path)); - - if (extractSignerCerts) { - canonCs = new CodeSource(csUrl, getSignerCertificates(cs)); - } else { - canonCs = new CodeSource(csUrl, cs.getCertificates()); - } - } catch (IOException ioe) { - // leave codesource as it is, unless we have to extract its - // signer certificates - if (extractSignerCerts) { - canonCs = new CodeSource(cs.getLocation(), getSignerCertificates(cs)); - } - } - } else { - if (extractSignerCerts) { - canonCs = new CodeSource(cs.getLocation(), getSignerCertificates(cs)); - } + if ("file".equals(url.getProtocol())) { + String path = url.getPath(); + path = canonicalizePath(path); + return new File(path).toURI().toURL(); } - return canonCs; + + return url; } - // Wrapper to return a canonical path that avoids calling getCanonicalPath() - // with paths that are intended to match all entries in the directory - private static String canonPath(String path) throws IOException { + private String canonicalizePath(String path) throws IOException { if (path.endsWith("*")) { - path = path.substring(0, path.length() - 1) + "-"; - path = new File(path).getCanonicalPath(); - return path.substring(0, path.length() - 1) + "*"; + path = path.substring(0, path.length() - 1); + String canonicalPath = new File(path).getCanonicalPath(); + return canonicalPath + "*"; } else { return new File(path).getCanonicalPath(); } } - /** - * return true if no replacement was performed, - * or if replacement succeeded. - */ - private boolean replacePrincipals(List principals, KeyStore keystore) { - - if (principals == null || principals.isEmpty() || keystore == null) return true; - - for (PolicyParser.PrincipalEntry pppe : principals) { - if (pppe.isReplaceName()) { - - // perform replacement - // (only X509 replacement is possible now) - String name; - if ((name = getDN(pppe.principalName, keystore)) == null) { - return false; - } - - pppe.principalClass = X500PRINCIPAL; - pppe.principalName = name; - } - } - // return true if no replacement was performed, - // or if replacement succeeded - return true; - } - - private void expandPermissionName(PolicyParser.PermissionEntry pe, KeyStore keystore) throws Exception { - // short cut the common case - if (pe.name == null || pe.name.indexOf("${{", 0) == -1) { - return; - } - - int startIndex = 0; - int b, e; - StringBuilder sb = new StringBuilder(); - while ((b = pe.name.indexOf("${{", startIndex)) != -1) { - e = pe.name.indexOf("}}", b); - if (e < 1) { - break; - } - sb.append(pe.name.substring(startIndex, b)); - - // get the value in ${{...}} - String value = pe.name.substring(b + 3, e); - - // parse up to the first ':' - int colonIndex; - String prefix = value; - String suffix; - if ((colonIndex = value.indexOf(':')) != -1) { - prefix = value.substring(0, colonIndex); - } - - // handle different prefix possibilities - if (prefix.equalsIgnoreCase("self")) { - // do nothing - handled later - sb.append(pe.name.substring(b, e + 2)); - startIndex = e + 2; - continue; - } else if (prefix.equalsIgnoreCase("alias")) { - // get the suffix and perform keystore alias replacement - if (colonIndex == -1) { - throw new Exception("Alias name not provided pe.name: " + pe.name); - } - suffix = value.substring(colonIndex + 1); - if ((suffix = getDN(suffix, keystore)) == null) { - throw new Exception("Unable to perform substitution on alias suffix: " + value.substring(colonIndex + 1)); - } - - sb.append(X500PRINCIPAL + " \"" + suffix + "\""); - startIndex = e + 2; - } else { - throw new Exception("Substitution value prefix unsupported: " + prefix); - } - } - - // copy the rest of the value - sb.append(pe.name.substring(startIndex)); - - pe.name = sb.toString(); - } - - private String getDN(String alias, KeyStore keystore) { - Certificate cert = null; - try { - cert = keystore.getCertificate(alias); - } catch (Exception e) { - return null; - } - - if (!(cert instanceof X509Certificate x509Cert)) { - return null; - } else { - // 4702543: X500 names with an EmailAddress - // were encoded incorrectly. create new - // X500Principal name with correct encoding - - X500Principal p = new X500Principal(x509Cert.getSubjectX500Principal().toString()); - return p.getName(); - } - } - - /** - * Each entry in the policy configuration file is represented by a - * PolicyEntry object.

- * - * A PolicyEntry is a (CodeSource,Permission) pair. The - * CodeSource contains the (URL, PublicKey) that together identify - * where the Java bytecodes come from and who (if anyone) signed - * them. The URL could refer to localhost. The URL could also be - * null, meaning that this policy entry is given to all comers, as - * long as they match the signer field. The signer could be null, - * meaning the code is not signed.

- * - * The Permission contains the (Type, Name, Action) triplet.

- * - * For now, the Policy object retrieves the public key from the - * X.509 certificate on disk that corresponds to the signedBy - * alias specified in the Policy config file. For reasons of - * efficiency, the Policy object keeps a hashtable of certs already - * read in. This could be replaced by a secure internal key - * store. - * - *

- * For example, the entry - *

-     *          permission java.io.File "/tmp", "read,write",
-     *          signedBy "Duke";
-     * 
- * is represented internally - *
-     *
-     * FilePermission f = new FilePermission("/tmp", "read,write");
-     * PublicKey p = publickeys.get("Duke");
-     * URL u = InetAddress.getLocalHost();
-     * CodeBase c = new CodeBase( p, u );
-     * pe = new PolicyEntry(f, c);
-     * 
- * - * @author Marianne Mueller - * @author Roland Schemers - * @see java.security.CodeSource - * @see java.security.Policy - * @see java.security.Permissions - * @see java.security.ProtectionDomain - */ private static class PolicyEntry { private final CodeSource codesource; final List permissions; - private final List principals; - /** - * Given a Permission and a CodeSource, create a policy entry. - * - * XXX Decide if/how to add validity fields and "purpose" fields to - * XXX policy entries - * - * @param cs the CodeSource, which encapsulates the URL and the - * public key - * attributes from the policy config file. Validity checks - * are performed on the public key before PolicyEntry is - * called. - * - */ - PolicyEntry(CodeSource cs, List principals) { + PolicyEntry(CodeSource cs) { this.codesource = cs; this.permissions = new ArrayList(); - this.principals = principals; // can be null - } - - PolicyEntry(CodeSource cs) { - this(cs, null); - } - - List getPrincipals() { - return principals; // can be null } /** @@ -1325,259 +381,6 @@ public String toString() { } } - private static class SelfPermission extends Permission { - - @java.io.Serial - private static final long serialVersionUID = -8315562579967246806L; - - /** - * The class name of the Permission class that will be - * created when this self permission is expanded . - * - * @serial - */ - private String type; - - /** - * The permission name. - * - * @serial - */ - private String name; - - /** - * The actions of the permission. - * - * @serial - */ - private String actions; - - /** - * The certs of the permission. - * - * @serial - */ - private Certificate[] certs; - - /** - * Creates a new SelfPermission containing the permission - * information needed later to expand the self - * @param type the class name of the Permission class that will be - * created when this permission is expanded and if necessary resolved. - * @param name the name of the permission. - * @param actions the actions of the permission. - * @param certs the certificates the permission's class was signed with. - * This is a list of certificate chains, where each chain is composed of - * a signer certificate and optionally its supporting certificate chain. - * Each chain is ordered bottom-to-top (i.e., with the signer - * certificate first and the (root) certificate authority last). - */ - public SelfPermission(String type, String name, String actions, Certificate[] certs) { - super(type); - if (type == null) { - throw new NullPointerException("Ttype cannot be null"); - } - this.type = type; - this.name = name; - this.actions = actions; - if (certs != null) { - // Extract the signer certs from the list of certificates. - for (int i = 0; i < certs.length; i++) { - if (!(certs[i] instanceof X509Certificate)) { - // there is no concept of signer certs, so we store the - // entire cert array - this.certs = certs.clone(); - break; - } - } - - if (this.certs == null) { - // Go through the list of certs and see if all the certs are - // signer certs. - int i = 0; - int count = 0; - while (i < certs.length) { - count++; - while (((i + 1) < certs.length) - && ((X509Certificate) certs[i]).getIssuerX500Principal() - .equals(((X509Certificate) certs[i + 1]).getSubjectX500Principal())) { - i++; - } - i++; - } - if (count == certs.length) { - // All the certs are signer certs, so we store the - // entire array - this.certs = certs.clone(); - } - - if (this.certs == null) { - // extract the signer certs - List signerCerts = new ArrayList<>(); - i = 0; - while (i < certs.length) { - signerCerts.add(certs[i]); - while (((i + 1) < certs.length) - && ((X509Certificate) certs[i]).getIssuerX500Principal() - .equals(((X509Certificate) certs[i + 1]).getSubjectX500Principal())) { - i++; - } - i++; - } - this.certs = new Certificate[signerCerts.size()]; - signerCerts.toArray(this.certs); - } - } - } - } - - /** - * This method always returns false for SelfPermission permissions. - * That is, an SelfPermission never considered to - * imply another permission. - * - * @param p the permission to check against. - * - * @return false. - */ - @Override - public boolean implies(Permission p) { - return false; - } - - /** - * Checks two SelfPermission objects for equality. - * - * Checks that obj is an SelfPermission, and has - * the same type (class) name, permission name, actions, and - * certificates as this object. - * - * @param obj the object we are testing for equality with this object. - * - * @return true if obj is an SelfPermission, and has the same - * type (class) name, permission name, actions, and - * certificates as this object. - */ - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - - if (!(obj instanceof SelfPermission)) return false; - SelfPermission that = (SelfPermission) obj; - - if (!(this.type.equals(that.type) && this.name.equals(that.name) && this.actions.equals(that.actions))) return false; - - if ((this.certs == null) && (that.certs == null)) { - return true; - } - - if ((this.certs == null) || (that.certs == null)) { - return false; - } - - if (this.certs.length != that.certs.length) { - return false; - } - - int i, j; - boolean match; - - for (i = 0; i < this.certs.length; i++) { - match = false; - for (j = 0; j < that.certs.length; j++) { - if (this.certs[i].equals(that.certs[j])) { - match = true; - break; - } - } - if (!match) return false; - } - - for (i = 0; i < that.certs.length; i++) { - match = false; - for (j = 0; j < this.certs.length; j++) { - if (that.certs[i].equals(this.certs[j])) { - match = true; - break; - } - } - if (!match) return false; - } - return true; - } - - /** - * Returns the hash code value for this object. - * - * @return a hash code value for this object. - */ - @Override - public int hashCode() { - int hash = type.hashCode(); - if (name != null) hash ^= name.hashCode(); - if (actions != null) hash ^= actions.hashCode(); - return hash; - } - - /** - * Returns the canonical string representation of the actions, - * which currently is the empty string "", since there are no actions - * for an SelfPermission. That is, the actions for the - * permission that will be created when this SelfPermission - * is resolved may be non-null, but an SelfPermission - * itself is never considered to have any actions. - * - * @return the empty string "". - */ - @Override - public String getActions() { - return ""; - } - - public String getSelfType() { - return type; - } - - public String getSelfName() { - return name; - } - - public String getSelfActions() { - return actions; - } - - public Certificate[] getCerts() { - return (certs == null ? null : certs.clone()); - } - - /** - * Returns a string describing this SelfPermission. The convention - * is to specify the class name, the permission name, and the actions, - * in the following format: '(unresolved "ClassName" "name" "actions")'. - * - * @return information about this SelfPermission. - */ - @Override - public String toString() { - return "(SelfPermission " + type + " " + name + " " + actions + ")"; - } - - /** - * Restores the state of this object from the stream. - * - * @param stream the {@code ObjectInputStream} from which data is read - * @throws IOException if an I/O error occurs - * @throws ClassNotFoundException if a serialized class cannot be loaded - */ - @java.io.Serial - private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { - stream.defaultReadObject(); - if (certs != null) { - this.certs = certs.clone(); - } - } - } - /** * holds policy information that we need to synch on */ @@ -1585,12 +388,8 @@ private static class PolicyInfo { // Stores grant entries in the policy final List policyEntries; - // Maps aliases to certs - final Map aliasMapping; - PolicyInfo(int numCaches) { policyEntries = new ArrayList<>(); - aliasMapping = Collections.synchronizedMap(new HashMap<>(11)); } } diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java index 9d5b0d5a13722..4e971bae93848 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java @@ -1,28 +1,3 @@ -/* - * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - /* * SPDX-License-Identifier: Apache-2.0 * @@ -33,1131 +8,168 @@ package org.opensearch.secure_sm.policy; -import javax.security.auth.x500.X500Principal; +import org.opensearch.secure_sm.policy.PropertyExpander.ExpandException; import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; import java.io.IOException; -import java.io.PrintWriter; import java.io.Reader; import java.io.StreamTokenizer; -import java.io.Writer; -import java.security.GeneralSecurityException; -import java.security.Principal; -import java.util.Collection; import java.util.Enumeration; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Locale; -import java.util.Map; -import java.util.StringTokenizer; -import java.util.TreeMap; import java.util.Vector; -/** - * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/provider/PolicyParser.java - */ public class PolicyParser { - private final Vector grantEntries; - private Map domainEntries; + private final Vector grantEntries = new Vector<>(); + private TokenStream tokenStream; - private StreamTokenizer st; - private int lookahead; - private boolean expandProp = false; - private String keyStoreUrlString = null; // unexpanded - private String keyStoreType = null; - private String keyStoreProvider = null; - private String storePassURL = null; - - private String expand(String value) throws PropertyExpander.ExpandException { - return expand(value, false); - } - - private String expand(String value, boolean encodeURL) throws PropertyExpander.ExpandException { - if (!expandProp) { - return value; - } else { - return PropertyExpander.expand(value, encodeURL); - } - } - - /** - * Creates a PolicyParser object. - */ - - public PolicyParser() { - grantEntries = new Vector<>(); - } - - public PolicyParser(boolean expandProp) { - this(); - this.expandProp = expandProp; - } - - /** - * Reads a policy configuration into the Policy object using a - * Reader object. - * - * @param policy the policy Reader object. - * - * @exception ParsingException if the policy configuration contains - * a syntax error. - * - * @exception IOException if an error occurs while reading the policy - * configuration. - */ + public PolicyParser() {} public void read(Reader policy) throws ParsingException, IOException { if (!(policy instanceof BufferedReader)) { policy = new BufferedReader(policy); } - /* - * Configure the stream tokenizer: - * Recognize strings between "..." - * Don't convert words to lowercase - * Recognize both C-style and C++-style comments - * Treat end-of-line as white space, not as a token - */ - st = new StreamTokenizer(policy); - - st.resetSyntax(); - st.wordChars('a', 'z'); - st.wordChars('A', 'Z'); - st.wordChars('.', '.'); - st.wordChars('0', '9'); - st.wordChars('_', '_'); - st.wordChars('$', '$'); - st.wordChars(128 + 32, 255); - st.whitespaceChars(0, ' '); - st.commentChar('/'); - st.quoteChar('\''); - st.quoteChar('"'); - st.lowerCaseMode(false); - st.ordinaryChar('/'); - st.slashSlashComments(true); - st.slashStarComments(true); - - /* - * The main parsing loop. The loop is executed once - * for each entry in the config file. The entries - * are delimited by semicolons. Once we've read in - * the information for an entry, go ahead and try to - * add it to the policy vector. - * - */ + tokenStream = new TokenStream(policy); - lookahead = st.nextToken(); - GrantEntry ge = null; - while (lookahead != StreamTokenizer.TT_EOF) { + while (!tokenStream.isEOF()) { if (peek("grant")) { - ge = parseGrantEntry(); - // could be null if we couldn't expand a property - if (ge != null) add(ge); - } else if (peek("keystore") && keyStoreUrlString == null) { - // only one keystore entry per policy file, others will be - // ignored - parseKeyStoreEntry(); - } else if (peek("keystorePasswordURL") && storePassURL == null) { - // only one keystore passwordURL per policy file, others will be - // ignored - parseStorePassURL(); - } else if (ge == null && keyStoreUrlString == null && storePassURL == null && peek("domain")) { - if (domainEntries == null) { - domainEntries = new TreeMap<>(); - } - DomainEntry de = parseDomainEntry(); - String domainName = de.getName(); - if (domainEntries.putIfAbsent(domainName, de) != null) { - Object[] source = { domainName }; - String msg = "duplicate keystore domain name: " + domainName; - throw new ParsingException(msg, source); - } + GrantNode grantNode = parseGrantEntry(); + addGrantNode(grantNode); } else { - // error? + throw new ParsingException(tokenStream.line(), "Expected 'grant'"); } - match(";"); - } - - if (keyStoreUrlString == null && storePassURL != null) { - throw new ParsingException("Keystore Password URL cannot be specified without also specifying keystore"); } } - public void add(GrantEntry ge) { - grantEntries.addElement(ge); - } - - public void replace(GrantEntry origGe, GrantEntry newGe) { - grantEntries.setElementAt(newGe, grantEntries.indexOf(origGe)); - } - - public boolean remove(GrantEntry ge) { - return grantEntries.removeElement(ge); - } - - /** - * Returns the (possibly expanded) keystore location, or null if the - * expansion fails. - */ - public String getKeyStoreUrl() { - try { - if (keyStoreUrlString != null && keyStoreUrlString.length() != 0) { - return expand(keyStoreUrlString, true).replace(File.separatorChar, '/'); - } - } catch (PropertyExpander.ExpandException peee) { - return null; + private boolean pollOnMatch(String expect) throws ParsingException, IOException { + if (peek(expect)) { + poll(expect); + return true; } - return null; - } - - public void setKeyStoreUrl(String url) { - keyStoreUrlString = url; - } - - public String getKeyStoreType() { - return keyStoreType; + return false; } - public void setKeyStoreType(String type) { - keyStoreType = type; + private boolean peek(String expected) throws IOException { + Token token = tokenStream.peek(); + return expected.equalsIgnoreCase(token.text); } - public String getKeyStoreProvider() { - return keyStoreProvider; - } + private String poll(String expected) throws IOException, ParsingException { + Token token = tokenStream.consume(); - public void setKeyStoreProvider(String provider) { - keyStoreProvider = provider; - } + // Match exact keyword or symbol + if (expected.equalsIgnoreCase("grant") + || expected.equalsIgnoreCase("Codebase") + || expected.equalsIgnoreCase("Permission") + || expected.equalsIgnoreCase("{") + || expected.equalsIgnoreCase("}") + || expected.equalsIgnoreCase(";") + || expected.equalsIgnoreCase(",")) { - public String getStorePassURL() { - try { - if (storePassURL != null && storePassURL.length() != 0) { - return expand(storePassURL, true).replace(File.separatorChar, '/'); + if (!expected.equalsIgnoreCase(token.text)) { + throw new ParsingException(token.line, expected, token.text); } - } catch (PropertyExpander.ExpandException peee) { - return null; - } - return null; - } - - public void setStorePassURL(String storePassURL) { - this.storePassURL = storePassURL; - } - - /** - * Enumerate all the entries in the global policy object. - * This method is used by policy admin tools. The tools - * should use the Enumeration methods on the returned object - * to fetch the elements sequentially. - */ - public Enumeration grantElements() { - return grantEntries.elements(); - } - - public Collection getDomainEntries() { - return domainEntries.values(); - } - - /** - * write out the policy - */ - - public void write(Writer policy) { - PrintWriter out = new PrintWriter(new BufferedWriter(policy)); - - out.println("/* AUTOMATICALLY GENERATED ON " + (new java.util.Date()) + "*/"); - out.println("/* DO NOT EDIT */"); - out.println(); - - // write the (unexpanded) keystore entry as the first entry of the - // policy file - if (keyStoreUrlString != null) { - writeKeyStoreEntry(out); - } - if (storePassURL != null) { - writeStorePassURL(out); - } - - // write "grant" entries - for (GrantEntry ge : grantEntries) { - ge.write(out); - out.println(); - } - out.flush(); - } - - /** - * parses a keystore entry - */ - private void parseKeyStoreEntry() throws ParsingException, IOException { - match("keystore"); - keyStoreUrlString = match("quoted string"); - - // parse keystore type - if (!peek(",")) { - return; // default type - } - match(","); - - if (peek("\"")) { - keyStoreType = match("quoted string"); - } else { - throw new ParsingException(st.lineno(), "Expected keystore type"); - } - - // parse keystore provider - if (!peek(",")) { - return; // provider optional + return token.text; } - match(","); - if (peek("\"")) { - keyStoreProvider = match("quoted string"); - } else { - throw new ParsingException(st.lineno(), "Keystore provider expected"); + if (token.type == StreamTokenizer.TT_WORD || token.type == '"' || token.type == '\'') { + return token.text; } - } - private void parseStorePassURL() throws ParsingException, IOException { - match("keyStorePasswordURL"); - storePassURL = match("quoted string"); + throw new ParsingException(token.line, expected, token.text); } - /** - * writes the (unexpanded) keystore entry - */ - private void writeKeyStoreEntry(PrintWriter out) { - out.print("keystore \""); - out.print(keyStoreUrlString); - out.print('"'); - if (keyStoreType != null && !keyStoreType.isEmpty()) out.print(", \"" + keyStoreType + "\""); - if (keyStoreProvider != null && !keyStoreProvider.isEmpty()) out.print(", \"" + keyStoreProvider + "\""); - out.println(";"); - out.println(); - } - - private void writeStorePassURL(PrintWriter out) { - out.print("keystorePasswordURL \""); - out.print(storePassURL); - out.print('"'); - out.println(";"); - out.println(); - } - - /** - * parse a Grant entry - */ - private GrantEntry parseGrantEntry() throws ParsingException, IOException { - GrantEntry e = new GrantEntry(); - LinkedList principals = null; - boolean ignoreEntry = false; - - match("grant"); + private GrantNode parseGrantEntry() throws ParsingException, IOException { + GrantNode grantNode = new GrantNode(); + poll("grant"); while (!peek("{")) { - - if (peekAndMatch("Codebase")) { - if (e.codeBase != null) throw new ParsingException(st.lineno(), "Multiple Codebase expressions"); - e.codeBase = match("quoted string"); - peekAndMatch(","); - } else if (peekAndMatch("SignedBy")) { - if (e.signedBy != null) throw new ParsingException(st.lineno(), "Multiple SignedBy expressions"); - e.signedBy = match("quoted string"); - - // verify syntax of the aliases - StringTokenizer aliases = new StringTokenizer(e.signedBy, ",", true); - int actr = 0; - int cctr = 0; - while (aliases.hasMoreTokens()) { - String alias = aliases.nextToken().trim(); - if (alias.equals(",")) cctr++; - else if (!alias.isEmpty()) actr++; - } - if (actr <= cctr) throw new ParsingException(st.lineno(), "SignedBy has an empty alias"); - - peekAndMatch(","); - } else if (peekAndMatch("Principal")) { - if (principals == null) { - principals = new LinkedList<>(); - } - - String principalClass; - String principalName; - - if (peek("\"")) { - // both the principalClass and principalName - // will be replaced later - principalClass = PrincipalEntry.REPLACE_NAME; - principalName = match("principal type"); - } else { - // check for principalClass wildcard - if (peek("*")) { - match("*"); - principalClass = PrincipalEntry.WILDCARD_CLASS; - } else { - principalClass = match("principal type"); - } - - // check for principalName wildcard - if (peek("*")) { - match("*"); - principalName = PrincipalEntry.WILDCARD_NAME; - } else { - principalName = match("quoted string"); - } - - // disallow WILDCARD_CLASS && actual name - if (principalClass.equals(PrincipalEntry.WILDCARD_CLASS) && !principalName.equals(PrincipalEntry.WILDCARD_NAME)) { - throw new ParsingException(st.lineno(), "Cannot specify Principal with a wildcard class without a wildcard name"); - } + if (pollOnMatch("Codebase")) { + if (grantNode.codeBase != null) { + throw new ParsingException(tokenStream.line(), "Multiple Codebase expressions"); } + String rawCodebase = poll(tokenStream.peek().text); try { - principalName = expand(principalName); - - if (principalClass.equals("javax.security.auth.x500.X500Principal") - && !principalName.equals(PrincipalEntry.WILDCARD_NAME)) { - - // 4702543: X500 names with an EmailAddress - // were encoded incorrectly. construct a new - // X500Principal with correct encoding. - - X500Principal p = new X500Principal((new X500Principal(principalName)).toString()); - principalName = p.getName(); - } - - principals.add(new PrincipalEntry(principalClass, principalName)); - } catch (PropertyExpander.ExpandException peee) { - ignoreEntry = true; + grantNode.codeBase = PropertyExpander.expand(rawCodebase, true).replace(File.separatorChar, '/'); + } catch (ExpandException e) { + e.printStackTrace(); } - peekAndMatch(","); - + pollOnMatch(","); } else { - throw new ParsingException(st.lineno(), "Expected codeBase or SignedBy or Principal"); + throw new ParsingException(tokenStream.line(), "Expected codeBase"); } } - if (principals != null) e.principals = principals; - match("{"); + poll("{"); while (!peek("}")) { if (peek("Permission")) { - try { - PermissionEntry pe = parsePermissionEntry(); - e.add(pe); - } catch (PropertyExpander.ExpandException peee) { - skipEntry(); // BugId 4219343 - } - match(";"); + PermissionNode permissionEntry = parsePermissionEntry(); + grantNode.add(permissionEntry); + poll(";"); } else { - throw new ParsingException(st.lineno(), "Expected permission entry"); - } - } - match("}"); - - try { - if (e.signedBy != null) e.signedBy = expand(e.signedBy); - if (e.codeBase != null) { - e.codeBase = expand(e.codeBase, true).replace(File.separatorChar, '/'); - } - } catch (PropertyExpander.ExpandException peee) { - return null; - } - - return (ignoreEntry) ? null : e; - } - - /** - * parse a Permission entry - */ - private PermissionEntry parsePermissionEntry() throws ParsingException, IOException, PropertyExpander.ExpandException { - PermissionEntry e = new PermissionEntry(); - - // Permission - match("Permission"); - e.permission = match("permission type"); - - if (peek("\"")) { - // Permission name - e.name = expand(match("quoted string")); - } - - if (!peek(",")) { - return e; - } - match(","); - - if (peek("\"")) { - e.action = expand(match("quoted string")); - if (!peek(",")) { - return e; - } - match(","); - } - - if (peekAndMatch("SignedBy")) { - e.signedBy = expand(match("quoted string")); - } - return e; - } - - /** - * parse a domain entry - */ - private DomainEntry parseDomainEntry() throws ParsingException, IOException { - DomainEntry domainEntry; - String name; - Map properties = new HashMap<>(); - - match("domain"); - name = match("domain name"); - - while (!peek("{")) { - // get the domain properties - properties = parseProperties("{"); - } - match("{"); - domainEntry = new DomainEntry(name, properties); - - while (!peek("}")) { - - match("keystore"); - name = match("keystore name"); - // get the keystore properties - if (!peek("}")) { - properties = parseProperties(";"); - } - match(";"); - domainEntry.add(new KeyStoreEntry(name, properties)); - } - match("}"); - - return domainEntry; - } - - /* - * Return a collection of domain properties or keystore properties. - */ - private Map parseProperties(String terminator) throws ParsingException, IOException { - - Map properties = new HashMap<>(); - String key; - String value; - while (!peek(terminator)) { - key = match("property name"); - match("="); - - try { - value = expand(match("quoted string")); - } catch (PropertyExpander.ExpandException peee) { - throw new IOException(peee.getLocalizedMessage()); - } - properties.put(key.toLowerCase(Locale.ENGLISH), value); - } - - return properties; - } - - private boolean peekAndMatch(String expect) throws ParsingException, IOException { - if (peek(expect)) { - match(expect); - return true; - } else { - return false; - } - } - - private boolean peek(String expect) { - boolean found = false; - - switch (lookahead) { - - case StreamTokenizer.TT_WORD: - if (expect.equalsIgnoreCase(st.sval)) found = true; - break; - case ',': - if (expect.equalsIgnoreCase(",")) found = true; - break; - case '{': - if (expect.equalsIgnoreCase("{")) found = true; - break; - case '}': - if (expect.equalsIgnoreCase("}")) found = true; - break; - case '"': - if (expect.equalsIgnoreCase("\"")) found = true; - break; - case '*': - if (expect.equalsIgnoreCase("*")) found = true; - break; - case ';': - if (expect.equalsIgnoreCase(";")) found = true; - break; - default: - - } - return found; - } - - private String match(String expect) throws ParsingException, IOException { - String value = null; - - switch (lookahead) { - case StreamTokenizer.TT_NUMBER: - throw new ParsingException(st.lineno(), expect); - case StreamTokenizer.TT_EOF: - Object[] source = { expect }; - String msg = "expected [" + expect + "], read [end of file]"; - throw new ParsingException(msg, source); - case StreamTokenizer.TT_WORD: - if (expect.equalsIgnoreCase(st.sval)) { - lookahead = st.nextToken(); - } else if (expect.equalsIgnoreCase("permission type")) { - value = st.sval; - lookahead = st.nextToken(); - } else if (expect.equalsIgnoreCase("principal type")) { - value = st.sval; - lookahead = st.nextToken(); - } else if (expect.equalsIgnoreCase("domain name") - || expect.equalsIgnoreCase("keystore name") - || expect.equalsIgnoreCase("property name")) { - value = st.sval; - lookahead = st.nextToken(); - } else { - throw new ParsingException(st.lineno(), expect, st.sval); - } - break; - case '"': - if (expect.equalsIgnoreCase("quoted string")) { - value = st.sval; - lookahead = st.nextToken(); - } else if (expect.equalsIgnoreCase("permission type")) { - value = st.sval; - lookahead = st.nextToken(); - } else if (expect.equalsIgnoreCase("principal type")) { - value = st.sval; - lookahead = st.nextToken(); - } else { - throw new ParsingException(st.lineno(), expect, st.sval); - } - break; - case ',': - if (expect.equalsIgnoreCase(",")) lookahead = st.nextToken(); - else throw new ParsingException(st.lineno(), expect, ","); - break; - case '{': - if (expect.equalsIgnoreCase("{")) lookahead = st.nextToken(); - else throw new ParsingException(st.lineno(), expect, "{"); - break; - case '}': - if (expect.equalsIgnoreCase("}")) lookahead = st.nextToken(); - else throw new ParsingException(st.lineno(), expect, "}"); - break; - case ';': - if (expect.equalsIgnoreCase(";")) lookahead = st.nextToken(); - else throw new ParsingException(st.lineno(), expect, ";"); - break; - case '*': - if (expect.equalsIgnoreCase("*")) lookahead = st.nextToken(); - else throw new ParsingException(st.lineno(), expect, "*"); - break; - case '=': - if (expect.equalsIgnoreCase("=")) lookahead = st.nextToken(); - else throw new ParsingException(st.lineno(), expect, "="); - break; - default: - throw new ParsingException(st.lineno(), expect, String.valueOf((char) lookahead)); - } - return value; - } - - /** - * skip all tokens for this entry leaving the delimiter ";" - * in the stream. - */ - private void skipEntry() throws ParsingException, IOException { - while (lookahead != ';') { - switch (lookahead) { - case StreamTokenizer.TT_NUMBER: - throw new ParsingException(st.lineno(), ";"); - case StreamTokenizer.TT_EOF: - throw new ParsingException("Expected read end of file"); - default: - lookahead = st.nextToken(); + throw new ParsingException(tokenStream.line(), "Expected permission entry"); } } - } - - /** - * Each grant entry in the policy configuration file is - * represented by a GrantEntry object. - * - *

- * For example, the entry - *

-     *      grant signedBy "Duke" {
-     *          permission java.io.FilePermission "/tmp", "read,write";
-     *      };
-     *
-     * 
- * is represented internally - *
-     *
-     * pe = new PermissionEntry("java.io.FilePermission",
-     *                           "/tmp", "read,write");
-     *
-     * ge = new GrantEntry("Duke", null);
-     *
-     * ge.add(pe);
-     *
-     * 
- * - * @author Roland Schemers - * - * version 1.19, 05/21/98 - */ - public static class GrantEntry { + poll("}"); - public String signedBy; - public String codeBase; - public LinkedList principals; - public Vector permissionEntries; - - public GrantEntry() { - principals = new LinkedList<>(); - permissionEntries = new Vector<>(); - } - - public GrantEntry(String signedBy, String codeBase) { - this.codeBase = codeBase; - this.signedBy = signedBy; - principals = new LinkedList<>(); - permissionEntries = new Vector<>(); + if (peek(";")) { + poll(";"); } - public void add(PermissionEntry pe) { - permissionEntries.addElement(pe); - } - - public boolean remove(PrincipalEntry pe) { - return principals.remove(pe); - } - - public boolean remove(PermissionEntry pe) { - return permissionEntries.removeElement(pe); - } - - public boolean contains(PrincipalEntry pe) { - return principals.contains(pe); - } - - public boolean contains(PermissionEntry pe) { - return permissionEntries.contains(pe); - } - - /** - * Enumerate all the permission entries in this GrantEntry. - */ - public Enumeration permissionElements() { - return permissionEntries.elements(); - } - - public void write(PrintWriter out) { - out.print("grant"); - if (signedBy != null) { - out.print(" signedBy \""); - out.print(signedBy); - out.print('"'); - if (codeBase != null) out.print(", "); - } - if (codeBase != null) { - out.print(" codeBase \""); - out.print(codeBase); - out.print('"'); - if (principals != null && principals.size() > 0) out.print(",\n"); - } - if (principals != null && principals.size() > 0) { - Iterator pli = principals.iterator(); - while (pli.hasNext()) { - out.print(" "); - PrincipalEntry pe = pli.next(); - pe.write(out); - if (pli.hasNext()) out.print(",\n"); - } - } - out.println(" {"); - for (PermissionEntry pe : permissionEntries) { - out.write(" "); - pe.write(out); - } - out.println("};"); + if (grantNode.codeBase != null) { + grantNode.codeBase = grantNode.codeBase.replace(File.separatorChar, '/'); } - public Object clone() { - GrantEntry ge = new GrantEntry(); - ge.codeBase = this.codeBase; - ge.signedBy = this.signedBy; - ge.principals = new LinkedList<>(this.principals); - ge.permissionEntries = new Vector<>(this.permissionEntries); - return ge; - } + return grantNode; } - /** - * Principal info (class and name) in a grant entry - */ - public static class PrincipalEntry implements Principal { - - public static final String WILDCARD_CLASS = "WILDCARD_PRINCIPAL_CLASS"; - public static final String WILDCARD_NAME = "WILDCARD_PRINCIPAL_NAME"; - public static final String REPLACE_NAME = "PolicyParser.REPLACE_NAME"; - - String principalClass; - String principalName; - - /** - * A PrincipalEntry consists of the Principal class and Principal name. - * - * @param principalClass the Principal class - * @param principalName the Principal name - * @throws NullPointerException if principalClass or principalName - * are null - */ - public PrincipalEntry(String principalClass, String principalName) { - if (principalClass == null || principalName == null) throw new NullPointerException("principalClass or principalName is null"); - this.principalClass = principalClass; - this.principalName = principalName; - } - - boolean isWildcardName() { - return principalName.equals(WILDCARD_NAME); - } - - boolean isWildcardClass() { - return principalClass.equals(WILDCARD_CLASS); - } - - boolean isReplaceName() { - return principalClass.equals(REPLACE_NAME); - } - - public String getPrincipalClass() { - return principalClass; - } - - public String getPrincipalName() { - return principalName; - } - - public String getDisplayClass() { - if (isWildcardClass()) { - return "*"; - } else if (isReplaceName()) { - return ""; - } else return principalClass; - } - - public String getDisplayName() { - return getDisplayName(false); - } - - public String getDisplayName(boolean addQuote) { - if (isWildcardName()) { - return "*"; - } else { - if (addQuote) return "\"" + principalName + "\""; - else return principalName; - } - } - - @Override - public String getName() { - return principalName; - } + private PermissionNode parsePermissionEntry() throws ParsingException, IOException { + PermissionNode permissionEntry = new PermissionNode(); + poll("Permission"); + permissionEntry.permission = poll(tokenStream.peek().text); - @Override - public String toString() { - if (!isReplaceName()) { - return getDisplayClass() + "/" + getDisplayName(); - } else { - return getDisplayName(); - } + if (isQuotedToken(tokenStream.peek())) { + permissionEntry.name = poll(tokenStream.peek().text); } - /** - * Test for equality between the specified object and this object. - * Two PrincipalEntries are equal if their class and name values - * are equal. - * - * @param obj the object to test for equality with this object - * @return true if the objects are equal, false otherwise - */ - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - - if (!(obj instanceof PrincipalEntry that)) return false; - - return (principalClass.equals(that.principalClass) && principalName.equals(that.principalName)); + if (peek(",")) { + poll(","); } - /** - * Return a hashcode for this PrincipalEntry. - * - * @return a hashcode for this PrincipalEntry - */ - @Override - public int hashCode() { - return principalClass.hashCode(); + if (isQuotedToken(tokenStream.peek())) { + permissionEntry.action = poll(tokenStream.peek().text); } - public void write(PrintWriter out) { - out.print("principal " + getDisplayClass() + " " + getDisplayName(true)); - } + return permissionEntry; } - /** - * Each permission entry in the policy configuration file is - * represented by a - * PermissionEntry object. - * - *

- * For example, the entry - *

-     *          permission java.io.FilePermission "/tmp", "read,write";
-     * 
- * is represented internally - *
-     *
-     * pe = new PermissionEntry("java.io.FilePermission",
-     *                           "/tmp", "read,write");
-     * 
- * - * @author Roland Schemers - * - * version 1.19, 05/21/98 - */ - - public static class PermissionEntry { - - public String permission; - public String name; - public String action; - public String signedBy; - - public PermissionEntry() {} - - public PermissionEntry(String permission, String name, String action) { - this.permission = permission; - this.name = name; - this.action = action; - } - - /** - * Calculates a hash code value for the object. Objects - * which are equal will also have the same hashcode. - */ - @Override - public int hashCode() { - int retval = permission.hashCode(); - if (name != null) retval ^= name.hashCode(); - if (action != null) retval ^= action.hashCode(); - return retval; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - - if (!(obj instanceof PermissionEntry that)) return false; - - if (this.permission == null) { - if (that.permission != null) return false; - } else { - if (!this.permission.equals(that.permission)) return false; - } - - if (this.name == null) { - if (that.name != null) return false; - } else { - if (!this.name.equals(that.name)) return false; - } - - if (this.action == null) { - if (that.action != null) return false; - } else { - if (!this.action.equals(that.action)) return false; - } - - if (this.signedBy == null) { - return that.signedBy == null; - } else { - return this.signedBy.equals(that.signedBy); - } - } - - public void write(PrintWriter out) { - out.print("permission "); - out.print(permission); - if (name != null) { - out.print(" \""); - - // ATTENTION: regex with double escaping, - // the normal forms look like: - // $name =~ s/\\/\\\\/g; and - // $name =~ s/\"/\\\"/g; - // and then in a java string, it's escaped again - - out.print(name.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\\\"")); - out.print('"'); - } - if (action != null) { - out.print(", \""); - out.print(action); - out.print('"'); - } - if (signedBy != null) { - out.print(", signedBy \""); - out.print(signedBy); - out.print('"'); - } - out.println(";"); - } + private boolean isQuotedToken(Token token) { + return token.type == '"' || token.type == '\''; } - /** - * Each domain entry in the keystore domain configuration file is - * represented by a DomainEntry object. - */ - static class DomainEntry { - private final String name; - private final Map properties; - private final Map entries; - - DomainEntry(String name, Map properties) { - this.name = name; - this.properties = properties; - entries = new HashMap<>(); - } - - String getName() { - return name; - } - - Map getProperties() { - return properties; - } - - Collection getEntries() { - return entries.values(); - } - - void add(KeyStoreEntry entry) throws ParsingException { - String keystoreName = entry.getName(); - if (!entries.containsKey(keystoreName)) { - entries.put(keystoreName, entry); - } else { - Object[] source = { keystoreName }; - String msg = "duplicate keystore name: " + keystoreName; - throw new ParsingException(msg, source); - } - } - - @Override - public String toString() { - StringBuilder s = new StringBuilder("\ndomain ").append(name); - - if (properties != null) { - for (Map.Entry property : properties.entrySet()) { - s.append("\n ").append(property.getKey()).append('=').append(property.getValue()); - } - } - s.append(" {\n"); - - for (KeyStoreEntry entry : entries.values()) { - s.append(entry).append("\n"); - } - s.append("}"); - - return s.toString(); - } + public void addGrantNode(GrantNode grantNode) { + grantEntries.addElement(grantNode); } - /** - * Each keystore entry in the keystore domain configuration file is - * represented by a KeyStoreEntry object. - */ - - static class KeyStoreEntry { - private final String name; - private final Map properties; - - KeyStoreEntry(String name, Map properties) { - this.name = name; - this.properties = properties; - } - - String getName() { - return name; - } - - Map getProperties() { - return properties; - } - - @Override - public String toString() { - StringBuilder s = new StringBuilder("\n keystore ").append(name); - if (properties != null) { - for (Map.Entry property : properties.entrySet()) { - s.append("\n ").append(property.getKey()).append('=').append(property.getValue()); - } - } - s.append(";"); - - return s.toString(); - } + public Enumeration grantElements() { + return grantEntries.elements(); } - public static class ParsingException extends GeneralSecurityException { - - @java.io.Serial - private static final long serialVersionUID = -4330692689482574072L; - - @SuppressWarnings("serial") // Not statically typed as Serializable - private Object[] source; - - /** - * Constructs a ParsingException with the specified - * detail message. A detail message is a String that describes - * this particular exception, which may, for example, specify which - * algorithm is not available. - * - * @param msg the detail message. - */ - public ParsingException(String msg) { - super(msg); - } - - public ParsingException(String msg, Object[] source) { - super(msg); - this.source = source; + public static class ParsingException extends Exception { + public ParsingException(String message) { + super(message); } - public ParsingException(int line, String msg) { - super("line " + line + ": " + msg); - source = new Object[] { line, msg }; + public ParsingException(int line, String expected) { + super("line " + line + ": expected [" + expected + "]"); } - public ParsingException(int line, String expect, String actual) { - super("line " + line + ": expected [" + expect + "], found [" + actual + "]"); - source = new Object[] { line, expect, actual }; + public ParsingException(int line, String expected, String found) { + super("line " + line + ": expected [" + expected + "], found [" + found + "]"); } } } diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java deleted file mode 100644 index ed19379b697c0..0000000000000 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.secure_sm.policy; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.util.Arrays; - -/** - * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/util/PolicyUtil.java - */ -public class PolicyUtil { - - // standard PKCS11 KeyStore type - private static final String P11KEYSTORE = "PKCS11"; - - // reserved word - private static final String NONE = "NONE"; - - /* - * Fast path reading from file urls in order to avoid calling - * FileURLConnection.connect() which can be quite slow the first time - * it is called. We really should clean up FileURLConnection so that - * this is not a problem but in the meantime this fix helps reduce - * start up time noticeably for the new launcher. -- DAC - */ - public static InputStream getInputStream(URL url) throws IOException { - if ("file".equals(url.getProtocol())) { - String path = url.getFile().replace('/', File.separatorChar); - path = ParseUtil.decode(path); - return new FileInputStream(path); - } else { - return url.openStream(); - } - } - - /** - * this is intended for use by the policy parser to - * instantiate a KeyStore from the information in the GUI/policy file - */ - public static KeyStore getKeyStore( - URL policyUrl, // URL of policy file - String keyStoreName, // input: keyStore URL - String keyStoreType, // input: keyStore type - String keyStoreProvider, // input: keyStore provider - String storePassURL // input: keyStore password - ) throws KeyStoreException, IOException, NoSuchProviderException, NoSuchAlgorithmException, java.security.cert.CertificateException { - - if (keyStoreName == null) { - throw new IllegalArgumentException("null KeyStore name"); - } - - char[] keyStorePassword = null; - try { - KeyStore ks; - if (keyStoreType == null) { - keyStoreType = KeyStore.getDefaultType(); - } - - if (P11KEYSTORE.equalsIgnoreCase(keyStoreType) && !NONE.equals(keyStoreName)) { - throw new IllegalArgumentException( - "Invalid value (" - + keyStoreName - + ") for keystore URL. If the keystore type is \"" - + P11KEYSTORE - + "\", the keystore url must be \"" - + NONE - + "\"" - ); - } - - if (keyStoreProvider != null) { - ks = KeyStore.getInstance(keyStoreType, keyStoreProvider); - } else { - ks = KeyStore.getInstance(keyStoreType); - } - - if (storePassURL != null) { - URL passURL; - try { - @SuppressWarnings("deprecation") - var _unused = passURL = new URL(storePassURL); - // absolute URL - } catch (MalformedURLException e) { - // relative URL - if (policyUrl == null) { - throw e; - } - @SuppressWarnings("deprecation") - var _unused = passURL = new URL(policyUrl, storePassURL); - } - - try (InputStream in = passURL.openStream()) { - keyStorePassword = Password.readPassword(in); - } - } - - if (NONE.equals(keyStoreName)) { - ks.load(null, keyStorePassword); - } else { - /* - * location of keystore is specified as absolute URL in policy - * file, or is relative to URL of policy file - */ - URL keyStoreUrl; - try { - @SuppressWarnings("deprecation") - var _unused = keyStoreUrl = new URL(keyStoreName); - // absolute URL - } catch (MalformedURLException e) { - // relative URL - if (policyUrl == null) { - throw e; - } - @SuppressWarnings("deprecation") - var _unused = keyStoreUrl = new URL(policyUrl, keyStoreName); - } - - try (InputStream inStream = new BufferedInputStream(getInputStream(keyStoreUrl))) { - ks.load(inStream, keyStorePassword); - } - } - return ks; - } finally { - if (keyStorePassword != null) { - Arrays.fill(keyStorePassword, ' '); - } - } - } -} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java index 759822b0ef2b5..fe438dfbb0db0 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java @@ -1,28 +1,3 @@ -/* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - /* * SPDX-License-Identifier: Apache-2.0 * @@ -35,6 +10,8 @@ import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; /** @@ -105,10 +82,10 @@ public static String expand(String value, boolean encodeURL) throws ExpandExcept // at the beginning of the string buffer try { if (sb.length() > 0 || !(new URI(val)).isAbsolute()) { - val = ParseUtil.encodePath(val); + val = URLEncoder.encode(val, StandardCharsets.UTF_8); } } catch (URISyntaxException use) { - val = ParseUtil.encodePath(val); + val = URLEncoder.encode(val, StandardCharsets.UTF_8); } } sb.append(val); diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java deleted file mode 100644 index 39e8efd87868c..0000000000000 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.secure_sm.policy; - -import java.lang.reflect.ReflectPermission; -import java.net.NetPermission; -import java.net.SocketPermission; -import java.security.AllPermission; -import java.security.SecurityPermission; - -/** - * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/util/SecurityConstants.java - */ -public final class SecurityConstants { - // Cannot create one of these - private SecurityConstants() {} - - // Commonly used string constants for permission actions used by - // SecurityManager. Declare here for shortcut when checking permissions - // in FilePermission, SocketPermission, and PropertyPermission. - - public static final String FILE_DELETE_ACTION = "delete"; - public static final String FILE_EXECUTE_ACTION = "execute"; - public static final String FILE_READ_ACTION = "read"; - public static final String FILE_WRITE_ACTION = "write"; - public static final String FILE_READLINK_ACTION = "readlink"; - - public static final String SOCKET_RESOLVE_ACTION = "resolve"; - public static final String SOCKET_CONNECT_ACTION = "connect"; - public static final String SOCKET_LISTEN_ACTION = "listen"; - public static final String SOCKET_ACCEPT_ACTION = "accept"; - public static final String SOCKET_CONNECT_ACCEPT_ACTION = "connect,accept"; - - public static final String PROPERTY_RW_ACTION = "read,write"; - public static final String PROPERTY_READ_ACTION = "read"; - public static final String PROPERTY_WRITE_ACTION = "write"; - - // Permission constants used in the various checkPermission() calls in JDK. - - // java.lang.Class, java.lang.SecurityManager, java.lang.System, - // java.net.URLConnection, java.security.AllPermission, java.security.Policy, - // sun.security.provider.PolicyFile - public static final AllPermission ALL_PERMISSION = new AllPermission(); - - // java.net.URL - public static final NetPermission SPECIFY_HANDLER_PERMISSION = new NetPermission("specifyStreamHandler"); - - // java.net.ProxySelector - public static final NetPermission SET_PROXYSELECTOR_PERMISSION = new NetPermission("setProxySelector"); - - // java.net.ProxySelector - public static final NetPermission GET_PROXYSELECTOR_PERMISSION = new NetPermission("getProxySelector"); - - // java.net.CookieHandler - public static final NetPermission SET_COOKIEHANDLER_PERMISSION = new NetPermission("setCookieHandler"); - - // java.net.CookieHandler - public static final NetPermission GET_COOKIEHANDLER_PERMISSION = new NetPermission("getCookieHandler"); - - // java.net.ResponseCache - public static final NetPermission SET_RESPONSECACHE_PERMISSION = new NetPermission("setResponseCache"); - - // java.net.ResponseCache - public static final NetPermission GET_RESPONSECACHE_PERMISSION = new NetPermission("getResponseCache"); - - // java.net.ServerSocket, java.net.Socket - public static final NetPermission SET_SOCKETIMPL_PERMISSION = new NetPermission("setSocketImpl"); - - // java.lang.SecurityManager, sun.applet.AppletPanel - public static final RuntimePermission CREATE_CLASSLOADER_PERMISSION = new RuntimePermission("createClassLoader"); - - // java.lang.SecurityManager - public static final RuntimePermission CHECK_MEMBER_ACCESS_PERMISSION = new RuntimePermission("accessDeclaredMembers"); - - // java.lang.SecurityManager, sun.applet.AppletSecurity - public static final RuntimePermission MODIFY_THREAD_PERMISSION = new RuntimePermission("modifyThread"); - - // java.lang.SecurityManager, sun.applet.AppletSecurity - public static final RuntimePermission MODIFY_THREADGROUP_PERMISSION = new RuntimePermission("modifyThreadGroup"); - - // java.lang.Class - public static final RuntimePermission GET_PD_PERMISSION = new RuntimePermission("getProtectionDomain"); - - // java.lang.Class, java.lang.ClassLoader, java.lang.Thread - public static final RuntimePermission GET_CLASSLOADER_PERMISSION = new RuntimePermission("getClassLoader"); - - // java.lang.Thread - public static final RuntimePermission STOP_THREAD_PERMISSION = new RuntimePermission("stopThread"); - - // java.lang.Thread - public static final RuntimePermission GET_STACK_TRACE_PERMISSION = new RuntimePermission("getStackTrace"); - - // java.lang.Thread - public static final RuntimePermission SUBCLASS_IMPLEMENTATION_PERMISSION = new RuntimePermission("enableContextClassLoaderOverride"); - - // java.security.AccessControlContext - public static final SecurityPermission CREATE_ACC_PERMISSION = new SecurityPermission("createAccessControlContext"); - - // java.security.AccessControlContext - public static final SecurityPermission GET_COMBINER_PERMISSION = new SecurityPermission("getDomainCombiner"); - - // java.security.Policy, java.security.ProtectionDomain - public static final SecurityPermission GET_POLICY_PERMISSION = new SecurityPermission("getPolicy"); - - // java.lang.SecurityManager - public static final SocketPermission LOCAL_LISTEN_PERMISSION = new SocketPermission("localhost:0", SOCKET_LISTEN_ACTION); - - // java.lang.reflect.AccessibleObject - public static final ReflectPermission ACCESS_PERMISSION = new ReflectPermission("suppressAccessChecks"); - - // sun.reflect.ReflectionFactory - public static final RuntimePermission REFLECTION_FACTORY_ACCESS_PERMISSION = new RuntimePermission("reflectionFactoryAccess"); - -} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Token.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Token.java new file mode 100644 index 0000000000000..e9a129f513607 --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Token.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +public class Token { + int type; + String text; + int line; + + Token(int type, String text, int line) { + this.type = type; + this.text = text; + this.line = line; + } +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/TokenStream.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/TokenStream.java new file mode 100644 index 0000000000000..4681c8dfea2aa --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/TokenStream.java @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +import java.io.IOException; +import java.io.Reader; +import java.io.StreamTokenizer; +import java.util.ArrayDeque; +import java.util.Deque; + +public class TokenStream { + private final StreamTokenizer tokenizer; + private final Deque buffer = new ArrayDeque<>(); + + TokenStream(Reader reader) { + this.tokenizer = Tokenizer.configureTokenizer(reader); + } + + Token peek() throws IOException { + if (buffer.isEmpty()) { + buffer.push(nextToken()); + } + return buffer.peek(); + } + + Token consume() throws IOException { + return buffer.isEmpty() ? nextToken() : buffer.pop(); + } + + boolean isEOF() throws IOException { + Token t = peek(); + return t.type == StreamTokenizer.TT_EOF; + } + + int line() throws IOException { + return peek().line; + } + + private Token nextToken() throws IOException { + int type = tokenizer.nextToken(); + String text = switch (type) { + case StreamTokenizer.TT_WORD, '"', '\'' -> tokenizer.sval; + case StreamTokenizer.TT_EOF -> ""; + default -> Character.toString((char) type); + }; + return new Token(type, text, tokenizer.lineno()); + } +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Tokenizer.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Tokenizer.java new file mode 100644 index 0000000000000..3ac771ef5f29e --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/Tokenizer.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +import java.io.Reader; +import java.io.StreamTokenizer; + +public final class Tokenizer { + + private Tokenizer() {} + + /* + * Configure the stream tokenizer: + * Recognize strings between "..." + * Don't convert words to lowercase + * Recognize both C-style and C++-style comments + * Treat end-of-line as white space, not as a token + */ + + // new Token(StreamTokenizer.TT_WORD, "grant", line) // keyword + // new Token(StreamTokenizer.TT_WORD, "Codebase", line) + // new Token('"', "file:/some/path", line) // quoted string + // new Token('{', "{", line) // symbol + // new Token(StreamTokenizer.TT_WORD, "permission", line) + // new Token(StreamTokenizer.TT_WORD, "java.io.FilePermission", line) + // new Token('"', "file", line) + // new Token(',', ",", line) + // new Token('"', "read", line) + // new Token(';', ";", line) + // new Token('}', "}", line) + // new Token(';', ";", line) + public static StreamTokenizer configureTokenizer(Reader reader) { + StreamTokenizer st = new StreamTokenizer(reader); + + st.resetSyntax(); + st.wordChars('a', 'z'); + st.wordChars('A', 'Z'); + st.wordChars('.', '.'); + st.wordChars('0', '9'); + st.wordChars('_', '_'); + st.wordChars('$', '$'); + st.wordChars(128 + 32, 255); // extended chars + st.whitespaceChars(0, ' '); + st.commentChar('/'); + st.quoteChar('\''); + st.quoteChar('"'); + st.lowerCaseMode(false); + st.ordinaryChar('/'); + st.slashSlashComments(true); + st.slashStarComments(true); + + return st; + } +} From bec04c8c193d71d9243586c9a6799e32c5bb7f6c Mon Sep 17 00:00:00 2001 From: Gulshan Date: Thu, 3 Apr 2025 10:38:40 +0530 Subject: [PATCH 3/8] Disable system exit --- .../java/org/opensearch/javaagent/SystemExitInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SystemExitInterceptor.java b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SystemExitInterceptor.java index 95489fdea5f55..77b71864fbaec 100644 --- a/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SystemExitInterceptor.java +++ b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SystemExitInterceptor.java @@ -40,7 +40,7 @@ public static void intercept(int code) throws Exception { final Class caller = walker.getCallerClass(); if (!AgentPolicy.isClassThatCanExit(caller.getName())) { - throw new SecurityException("The class " + caller + " is not allowed to call System::exit(" + code + ")"); + // throw new SecurityException("The class " + caller + " is not allowed to call System::exit(" + code + ")"); } } } From 515bfc050ee0d6a6c6f7331d7afa0d8ff4a61f79 Mon Sep 17 00:00:00 2001 From: Gulshan Date: Thu, 3 Apr 2025 17:18:54 +0530 Subject: [PATCH 4/8] Only property expander change --- .../secure_sm/policy/PropertyExpander.java | 134 ++++++++---------- 1 file changed, 62 insertions(+), 72 deletions(-) diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java index fe438dfbb0db0..486fe9845dae8 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java @@ -8,22 +8,38 @@ package org.opensearch.secure_sm.policy; +import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.util.regex.MatchResult; +import java.util.regex.Pattern; -/** - * Adapted from: https://github.com/openjdk/jdk23u/blob/master/src/java.base/share/classes/sun/security/util/PropertyExpander.java - */ public class PropertyExpander { + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{\\{(?.*?)}}|\\$\\{(?.*?)}"); + public static class ExpandException extends GeneralSecurityException { private static final long serialVersionUID = -1L; - public ExpandException(String msg) { - super(msg); + public ExpandException(String message) { + super(message); + } + } + + private static class UncheckedExpandException extends RuntimeException { + private final ExpandException cause; + + UncheckedExpandException(ExpandException cause) { + super(cause); + this.cause = cause; + } + + @Override + public ExpandException getCause() { + return cause; } } @@ -32,79 +48,53 @@ public static String expand(String value) throws ExpandException { } public static String expand(String value, boolean encodeURL) throws ExpandException { - if (value == null) return null; + if (value == null || !value.contains("${")) { + return value; + } - int p = value.indexOf("${"); + try { + return PLACEHOLDER_PATTERN.matcher(value).replaceAll(match -> { + try { + return handleMatch(match, encodeURL); + } catch (ExpandException e) { + throw new UncheckedExpandException(e); + } + }); + } catch (UncheckedExpandException e) { + throw e.getCause(); + } + } - // no special characters - if (p == -1) return value; + private static String handleMatch(MatchResult match, boolean encodeURL) throws ExpandException { + String escaped = match.group("escaped"); + if (escaped != null) { + // Preserve escaped placeholders like ${{...}} + return "${{" + escaped + "}}"; + } - StringBuilder sb = new StringBuilder(value.length()); - int max = value.length(); - int i = 0; // index of last character we copied + String placeholder = match.group("normal"); + return expandPlaceholder(placeholder, encodeURL); + } - scanner: while (p < max) { - if (p > i) { - // copy in anything before the special stuff - sb.append(value.substring(i, p)); - } - int pe = p + 2; - - // do not expand ${{ ... }} - if (pe < max && value.charAt(pe) == '{') { - pe = value.indexOf("}}", pe); - if (pe == -1 || pe + 2 == max) { - // append remaining chars - sb.append(value.substring(p)); - break scanner; - } else { - // append as normal text - pe++; - sb.append(value.substring(p, pe + 1)); - } - } else { - while ((pe < max) && (value.charAt(pe) != '}')) { - pe++; - } - if (pe == max) { - // no matching '}' found, just add in as normal text - sb.append(value.substring(p, pe)); - break scanner; + private static String expandPlaceholder(String placeholder, boolean encodeURL) throws ExpandException { + return switch (placeholder) { + case "/" -> String.valueOf(File.separatorChar); + default -> { + String value = System.getProperty(placeholder); + if (value == null) { + throw new ExpandException("Unable to expand property: " + placeholder); } - String prop = value.substring(p + 2, pe); - if (prop.equals("/")) { - sb.append(java.io.File.separatorChar); - } else { - String val = System.getProperty(prop); - if (val != null) { - if (encodeURL) { - // encode 'val' unless it's an absolute URI - // at the beginning of the string buffer - try { - if (sb.length() > 0 || !(new URI(val)).isAbsolute()) { - val = URLEncoder.encode(val, StandardCharsets.UTF_8); - } - } catch (URISyntaxException use) { - val = URLEncoder.encode(val, StandardCharsets.UTF_8); - } - } - sb.append(val); - } else { - throw new ExpandException("unable to expand property " + prop); - } - } - } - i = pe + 1; - p = value.indexOf("${", i); - if (p == -1) { - // no more to expand. copy in any extra - if (i < max) { - sb.append(value.substring(i, max)); - } - // break out of loop - break scanner; + yield encodeURL ? encodeValue(value) : value; } + }; + } + + private static String encodeValue(String value) { + try { + URI uri = new URI(value); + return uri.isAbsolute() ? value : URLEncoder.encode(value, StandardCharsets.UTF_8); + } catch (URISyntaxException e) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); } - return sb.toString(); } } From 1fb2a098392a07cc632c9efd6fa452526e018bf9 Mon Sep 17 00:00:00 2001 From: Gulshan Kumar Date: Thu, 3 Apr 2025 19:42:00 +0000 Subject: [PATCH 5/8] Better Exception handling --- .../secure_sm/policy/PolicyFile.java | 235 +++++++----------- .../policy/PolicyInitializationException.java | 27 ++ .../secure_sm/policy/PolicyParser.java | 42 +++- .../secure_sm/policy/PropertyExpander.java | 36 +-- 4 files changed, 167 insertions(+), 173 deletions(-) create mode 100644 libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyInitializationException.java diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java index 057eea60b2cdd..53433654f8f49 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java @@ -20,95 +20,69 @@ import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.security.AllPermission; import java.security.CodeSource; import java.security.Permission; import java.security.PermissionCollection; import java.security.Permissions; import java.security.ProtectionDomain; +import java.security.SecurityPermission; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Optional; +import java.util.PropertyPermission; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; @SuppressWarnings("removal") public class PolicyFile extends java.security.Policy { - public static final SocketPermission LOCAL_LISTEN_PERMISSION = new SocketPermission("localhost:0", "listen"); + public static final Set PERM_CLASSES_TO_SKIP = Set.of( + "org.opensearch.secure_sm.ThreadContextPermission", + "org.opensearch.secure_sm.ThreadPermission", + "org.opensearch.SpecialPermission", + "org.bouncycastle.crypto.CryptoServicesPermission", + "org.opensearch.script.ClassPermission", + "javax.security.auth.AuthPermission" + ); private static final int DEFAULT_CACHE_SIZE = 1; private volatile PolicyInfo policyInfo; private URL url; - /** - * When a policy file has a syntax error, the exception code may generate - * another permission check and this can cause the policy file to be parsed - * repeatedly, leading to a StackOverflowError or ClassCircularityError. - * To avoid this, this set is populated with policy files that have been - * previously parsed and have syntax errors, so that they can be - * subsequently ignored. - */ - private static Set badPolicyURLs = Collections.newSetFromMap(new ConcurrentHashMap()); - - /** - * Initializes the Policy object and reads the default policy - * from the specified URL only. - */ public PolicyFile(URL url) { this.url = url; - init(url); + try { + init(url); + } catch (PolicyInitializationException e) { + throw new RuntimeException("Failed to initialize policy file", e); + } } - /** - * Initializes the Policy object and reads the default policy - * configuration file(s) into the Policy object. - * - * See the class description for details on the algorithm used to - * initialize the Policy object. - */ - private void init(URL url) { + private void init(URL url) throws PolicyInitializationException { int numCaches = DEFAULT_CACHE_SIZE; PolicyInfo newInfo = new PolicyInfo(numCaches); initPolicyFile(newInfo, url); policyInfo = newInfo; } - private void initPolicyFile(final PolicyInfo newInfo, final URL url) { + private void initPolicyFile(final PolicyInfo newInfo, final URL url) throws PolicyInitializationException { init(url, newInfo); } - /** - * Reads a policy configuration into the Policy object using a - * Reader object. - */ - private void init(URL policy, PolicyInfo newInfo) { - if (badPolicyURLs.contains(policy)) { - return; - } - + private void init(URL policy, PolicyInfo newInfo) throws PolicyInitializationException { try (InputStreamReader reader = new InputStreamReader(getInputStream(policy), StandardCharsets.UTF_8)) { PolicyParser policyParser = new PolicyParser(); policyParser.read(reader); - Collections.list(policyParser.grantElements()).forEach(grantNode -> { - try { - addGrantNode(grantNode, newInfo); - } catch (Exception e) { - e.printStackTrace(System.err); - } - }); + for (GrantNode grantNode : Collections.list(policyParser.grantElements())) { + addGrantNode(grantNode, newInfo); + } - return; - } catch (PolicyParser.ParsingException pe) { - badPolicyURLs.add(policy); - pe.printStackTrace(System.err); - } catch (IOException ioe) { - ioe.printStackTrace(System.err); + } catch (Exception e) { + throw new PolicyInitializationException("Failed to load policy from: " + policy, e); } - - return; } public static InputStream getInputStream(URL url) throws IOException { @@ -121,38 +95,42 @@ public static InputStream getInputStream(URL url) throws IOException { } } - /** - * Given a GrantEntry, create a codeSource. - * - * @return null if signedBy alias is not recognized - */ - private CodeSource getCodeSource(GrantNode grantEntry, PolicyInfo newInfo) throws java.net.MalformedURLException { - Certificate[] certs = null; - URL location; - - if (grantEntry.codeBase != null) location = newURL(grantEntry.codeBase); - else location = null; - - return (canonicalizeCodebase(new CodeSource(location, certs))); + private CodeSource getCodeSource(GrantNode grantEntry, PolicyInfo newInfo) throws PolicyInitializationException { + try { + Certificate[] certs = null; + URL location = (grantEntry.codeBase != null) ? newURL(grantEntry.codeBase) : null; + return canonicalizeCodebase(new CodeSource(location, certs)); + } catch (Exception e) { + throw new PolicyInitializationException("Failed to get CodeSource", e); + } } - private void addGrantNode(GrantNode grantEntry, PolicyInfo newInfo) throws Exception { - + private void addGrantNode(GrantNode grantEntry, PolicyInfo newInfo) throws PolicyInitializationException { CodeSource codesource = getCodeSource(grantEntry, newInfo); - if (codesource == null) return; + if (codesource == null) { + throw new PolicyInitializationException("Null CodeSource for: " + grantEntry.codeBase); + } PolicyEntry entry = new PolicyEntry(codesource); Enumeration enum_ = grantEntry.permissionElements(); while (enum_.hasMoreElements()) { PermissionNode pe = enum_.nextElement(); + expandPermissionName(pe); try { - // Store the original name before expansion - expandPermissionName(pe); - Optional perm = getInstance(pe.permission, pe.name, pe.action); - perm.ifPresent(entry::add); - } catch (ClassNotFoundException cfne) { - cfne.printStackTrace(System.err); + if (perm.isPresent()) { + entry.add(perm.get()); + } + } catch (ClassNotFoundException e) { + + // these were mostly custom permission classes added for security + // manager. Since security manager is deprecated, we can skip these + // permissions classes. + if (PERM_CLASSES_TO_SKIP.contains(pe.permission)) { + continue; // skip this permission + } + + throw new PolicyInitializationException("Permission class not found: " + pe.permission, e); } } newInfo.policyEntries.add(entry); @@ -168,12 +146,9 @@ private void expandPermissionName(PermissionNode pe) { StringBuilder sb = new StringBuilder(); while ((b = pe.name.indexOf("${{", startIndex)) != -1 && (e = pe.name.indexOf("}}", b)) != -1) { - sb.append(pe.name, startIndex, b); String value = pe.name.substring(b + 3, e); - sb.append("${{").append(value).append("}}"); - startIndex = e + 2; } @@ -184,60 +159,56 @@ private void expandPermissionName(PermissionNode pe) { private static final Optional getInstance(String type, String name, String actions) throws ClassNotFoundException { Class pc = Class.forName(type, false, null); Permission answer = getKnownPermission(pc, name, actions); - if (answer != null) { - return Optional.of(answer); - } - return Optional.empty(); + return Optional.ofNullable(answer); } - /** - * Creates one of the well-known permissions in the java.base module - * directly instead of via reflection. Keep list short to not penalize - * permissions from other modules. - */ private static Permission getKnownPermission(Class claz, String name, String actions) { if (claz.equals(FilePermission.class)) { return new FilePermission(name, actions); } else if (claz.equals(SocketPermission.class)) { return new SocketPermission(name, actions); + } else if (claz.equals(RuntimePermission.class)) { + return new RuntimePermission(name, actions); + } else if (claz.equals(PropertyPermission.class)) { + return new PropertyPermission(name, actions); } else if (claz.equals(NetPermission.class)) { return new NetPermission(name, actions); + } else if (claz.equals(AllPermission.class)) { + return new AllPermission(); + } else if (claz.equals(SecurityPermission.class)) { + return new SecurityPermission(name, actions); } else { return null; } } - /** - * Refreshes the policy object by re-reading all the policy files. - */ @Override public void refresh() { - init(url); + try { + init(url); + } catch (PolicyInitializationException e) { + throw new RuntimeException("Failed to refresh policy", e); + } } @Override public boolean implies(ProtectionDomain pd, Permission p) { PermissionCollection pc = getPermissions(pd); - if (pc == null) { - return false; - } - - // cache mapping of protection domain to its PermissionCollection - return pc.implies(p); + return pc != null && pc.implies(p); } @Override public PermissionCollection getPermissions(ProtectionDomain domain) { Permissions perms = new Permissions(); - if (domain == null) return perms; - getPermissionsForProtectionDomain(perms, domain); + try { + getPermissionsForProtectionDomain(perms, domain); + } catch (PolicyInitializationException e) { + throw new RuntimeException("Failed to get permissions for domain", e); + } - // add static perms - // - adding static perms after policy perms is necessary - // to avoid a regression for 4301064 PermissionCollection pc = domain.getPermissions(); if (pc != null) { synchronized (pc) { @@ -253,12 +224,16 @@ public PermissionCollection getPermissions(ProtectionDomain domain) { @Override public PermissionCollection getPermissions(CodeSource codesource) { - if (codesource == null) { - return new Permissions(); - } + if (codesource == null) return new Permissions(); Permissions perms = new Permissions(); - CodeSource canonicalCodeSource = canonicalizeCodebase(codesource); + CodeSource canonicalCodeSource; + + try { + canonicalCodeSource = canonicalizeCodebase(codesource); + } catch (PolicyInitializationException e) { + throw new RuntimeException("Failed to canonicalize CodeSource", e); + } for (PolicyEntry entry : policyInfo.policyEntries) { if (entry.getCodeSource().implies(canonicalCodeSource)) { @@ -271,33 +246,30 @@ public PermissionCollection getPermissions(CodeSource codesource) { return perms; } - private PermissionCollection getPermissionsForProtectionDomain(Permissions perms, ProtectionDomain pd) { + private void getPermissionsForProtectionDomain(Permissions perms, ProtectionDomain pd) throws PolicyInitializationException { final CodeSource cs = pd.getCodeSource(); - if (cs == null) return perms; + if (cs == null) return; + + CodeSource canonicalCodeSource = canonicalizeCodebase(cs); for (PolicyEntry entry : policyInfo.policyEntries) { - if (entry.getCodeSource().implies(cs)) { + if (entry.getCodeSource().implies(canonicalCodeSource)) { for (Permission permission : entry.permissions) { perms.add(permission); } } } - - return perms; } - private CodeSource canonicalizeCodebase(CodeSource cs) { + private CodeSource canonicalizeCodebase(CodeSource cs) throws PolicyInitializationException { URL location = cs.getLocation(); - if (location == null) { - return cs; - } + if (location == null) return cs; try { URL canonicalUrl = canonicalizeUrl(location); return new CodeSource(canonicalUrl, cs.getCertificates()); } catch (IOException e) { - // Log the exception or handle it as appropriate - return cs; + throw new PolicyInitializationException("Failed to canonicalize CodeSource", e); } } @@ -312,7 +284,7 @@ private URL canonicalizeUrl(URL url) throws IOException { try { url = new URL(spec.substring(0, separator)); } catch (MalformedURLException e) { - // If unwrapping fails, keep the original URL + throw new IOException("Malformed nested jar URL", e); } } } @@ -329,35 +301,25 @@ private URL canonicalizeUrl(URL url) throws IOException { private String canonicalizePath(String path) throws IOException { if (path.endsWith("*")) { path = path.substring(0, path.length() - 1); - String canonicalPath = new File(path).getCanonicalPath(); - return canonicalPath + "*"; + return new File(path).getCanonicalPath() + "*"; } else { return new File(path).getCanonicalPath(); } } private static class PolicyEntry { - private final CodeSource codesource; final List permissions; PolicyEntry(CodeSource cs) { this.codesource = cs; - this.permissions = new ArrayList(); + this.permissions = new ArrayList<>(); } - /** - * add a Permission object to this entry. - * No need to sync add op because perms are added to entry only - * while entry is being initialized - */ void add(Permission p) { permissions.add(p); } - /** - * Return the CodeSource for this policy entry - */ CodeSource getCodeSource() { return codesource; } @@ -365,27 +327,16 @@ CodeSource getCodeSource() { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("{"); - sb.append(getCodeSource()); - sb.append("\n"); - for (int j = 0; j < permissions.size(); j++) { - Permission p = permissions.get(j); - sb.append(" "); - sb.append(" "); - sb.append(p); - sb.append("\n"); + sb.append("{").append(getCodeSource()).append("\n"); + for (Permission p : permissions) { + sb.append(" ").append(p).append("\n"); } - sb.append("}"); - sb.append("\n"); + sb.append("}\n"); return sb.toString(); } } - /** - * holds policy information that we need to synch on - */ private static class PolicyInfo { - // Stores grant entries in the policy final List policyEntries; PolicyInfo(int numCaches) { diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyInitializationException.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyInitializationException.java new file mode 100644 index 0000000000000..9205c0aecec41 --- /dev/null +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyInitializationException.java @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.secure_sm.policy; + +/** + * Custom exception for failures during policy file parsing, + */ +public class PolicyInitializationException extends Exception { + + public PolicyInitializationException(String message) { + super(message); + } + + public PolicyInitializationException(String message, Throwable cause) { + super(message, cause); + } + + public PolicyInitializationException(Throwable cause) { + super(cause); + } +} diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java index 4e971bae93848..3da39e18bd0a6 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java @@ -35,9 +35,10 @@ public void read(Reader policy) throws ParsingException, IOException { while (!tokenStream.isEOF()) { if (peek("grant")) { GrantNode grantNode = parseGrantEntry(); - addGrantNode(grantNode); - } else { - throw new ParsingException(tokenStream.line(), "Expected 'grant'"); + + if (grantNode != null) { + addGrantNode(grantNode); + } } } } @@ -94,7 +95,9 @@ private GrantNode parseGrantEntry() throws ParsingException, IOException { try { grantNode.codeBase = PropertyExpander.expand(rawCodebase, true).replace(File.separatorChar, '/'); } catch (ExpandException e) { - e.printStackTrace(); + // skip this grant as expansion failed due to missing expansion property. + skipCurrentGrantBlock(); + return null; } pollOnMatch(","); } else { @@ -127,6 +130,37 @@ private GrantNode parseGrantEntry() throws ParsingException, IOException { return grantNode; } + private void skipCurrentGrantBlock() throws IOException, ParsingException { + // Consume until we find a matching closing '}' + int braceDepth = 0; + + // Go until we find the initial '{' + while (!tokenStream.isEOF()) { + Token token = tokenStream.peek(); + if ("{".equals(token.text)) { + braceDepth++; + tokenStream.consume(); + break; + } + tokenStream.consume(); + } + + // Now consume until matching '}' + while (braceDepth > 0 && !tokenStream.isEOF()) { + Token token = tokenStream.consume(); + if ("{".equals(token.text)) { + braceDepth++; + } else if ("}".equals(token.text)) { + braceDepth--; + } + } + + // Consume optional trailing semicolon + if (peek(";")) { + poll(";"); + } + } + private PermissionNode parsePermissionEntry() throws ParsingException, IOException { PermissionNode permissionEntry = new PermissionNode(); poll("Permission"); diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java index 486fe9845dae8..757062d46f226 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java @@ -14,7 +14,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; -import java.util.regex.MatchResult; +import java.util.regex.Matcher; import java.util.regex.Pattern; public class PropertyExpander { @@ -29,20 +29,6 @@ public ExpandException(String message) { } } - private static class UncheckedExpandException extends RuntimeException { - private final ExpandException cause; - - UncheckedExpandException(ExpandException cause) { - super(cause); - this.cause = cause; - } - - @Override - public ExpandException getCause() { - return cause; - } - } - public static String expand(String value) throws ExpandException { return expand(value, false); } @@ -52,23 +38,19 @@ public static String expand(String value, boolean encodeURL) throws ExpandExcept return value; } - try { - return PLACEHOLDER_PATTERN.matcher(value).replaceAll(match -> { - try { - return handleMatch(match, encodeURL); - } catch (ExpandException e) { - throw new UncheckedExpandException(e); - } - }); - } catch (UncheckedExpandException e) { - throw e.getCause(); + Matcher matcher = PLACEHOLDER_PATTERN.matcher(value); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String replacement = handleMatch(matcher, encodeURL); + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); } + matcher.appendTail(sb); + return sb.toString(); } - private static String handleMatch(MatchResult match, boolean encodeURL) throws ExpandException { + private static String handleMatch(Matcher match, boolean encodeURL) throws ExpandException { String escaped = match.group("escaped"); if (escaped != null) { - // Preserve escaped placeholders like ${{...}} return "${{" + escaped + "}}"; } From 26ce31064c426e4245171c3f23a4ad3584d047f3 Mon Sep 17 00:00:00 2001 From: Gulshan Kumar Date: Thu, 3 Apr 2025 20:43:04 +0000 Subject: [PATCH 6/8] Address PR comments --- .../{GrantNode.java => GrantEntry.java} | 10 ++--- ...rmissionNode.java => PermissionEntry.java} | 4 +- .../secure_sm/policy/PolicyFile.java | 17 ++++---- .../secure_sm/policy/PolicyParser.java | 40 +++++++++---------- 4 files changed, 35 insertions(+), 36 deletions(-) rename libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/{GrantNode.java => GrantEntry.java} (81%) rename libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/{PermissionNode.java => PermissionEntry.java} (93%) diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantNode.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantEntry.java similarity index 81% rename from libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantNode.java rename to libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantEntry.java index 595a6fa4b7f13..3e0100c9db72b 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantNode.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantEntry.java @@ -12,15 +12,15 @@ import java.util.Enumeration; import java.util.LinkedList; -public class GrantNode { +public class GrantEntry { public String codeBase; - private final LinkedList permissionEntries = new LinkedList<>(); + private final LinkedList permissionEntries = new LinkedList<>(); - public void add(PermissionNode entry) { + public void add(PermissionEntry entry) { permissionEntries.add(entry); } - public Enumeration permissionElements() { + public Enumeration permissionElements() { return Collections.enumeration(permissionEntries); } @@ -32,7 +32,7 @@ public void write(PrintWriter out) { out.print("\""); } out.println(" {"); - for (PermissionNode pe : permissionEntries) { + for (PermissionEntry pe : permissionEntries) { out.print(" permission "); out.print(pe.permission); if (pe.name != null) { diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionNode.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionEntry.java similarity index 93% rename from libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionNode.java rename to libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionEntry.java index ca96f91586619..289a902a225ac 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionNode.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PermissionEntry.java @@ -10,7 +10,7 @@ import java.io.PrintWriter; import java.util.Objects; -public class PermissionNode { +public class PermissionEntry { public String permission; public String name; public String action; @@ -24,7 +24,7 @@ public int hashCode() { public boolean equals(Object obj) { if (obj == this) return true; - return obj instanceof PermissionNode that + return obj instanceof PermissionEntry that && Objects.equals(this.permission, that.permission) && Objects.equals(this.name, that.name) && Objects.equals(this.action, that.action); diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java index 53433654f8f49..ed4e4679ea5bb 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java @@ -44,7 +44,8 @@ public class PolicyFile extends java.security.Policy { "org.opensearch.SpecialPermission", "org.bouncycastle.crypto.CryptoServicesPermission", "org.opensearch.script.ClassPermission", - "javax.security.auth.AuthPermission" + "javax.security.auth.AuthPermission", + "javax.security.auth.kerberos.ServicePermission" ); private static final int DEFAULT_CACHE_SIZE = 1; @@ -76,8 +77,8 @@ private void init(URL policy, PolicyInfo newInfo) throws PolicyInitializationExc PolicyParser policyParser = new PolicyParser(); policyParser.read(reader); - for (GrantNode grantNode : Collections.list(policyParser.grantElements())) { - addGrantNode(grantNode, newInfo); + for (GrantEntry grantEntry : Collections.list(policyParser.grantElements())) { + addGrantEntry(grantEntry, newInfo); } } catch (Exception e) { @@ -95,7 +96,7 @@ public static InputStream getInputStream(URL url) throws IOException { } } - private CodeSource getCodeSource(GrantNode grantEntry, PolicyInfo newInfo) throws PolicyInitializationException { + private CodeSource getCodeSource(GrantEntry grantEntry, PolicyInfo newInfo) throws PolicyInitializationException { try { Certificate[] certs = null; URL location = (grantEntry.codeBase != null) ? newURL(grantEntry.codeBase) : null; @@ -105,16 +106,16 @@ private CodeSource getCodeSource(GrantNode grantEntry, PolicyInfo newInfo) throw } } - private void addGrantNode(GrantNode grantEntry, PolicyInfo newInfo) throws PolicyInitializationException { + private void addGrantEntry(GrantEntry grantEntry, PolicyInfo newInfo) throws PolicyInitializationException { CodeSource codesource = getCodeSource(grantEntry, newInfo); if (codesource == null) { throw new PolicyInitializationException("Null CodeSource for: " + grantEntry.codeBase); } PolicyEntry entry = new PolicyEntry(codesource); - Enumeration enum_ = grantEntry.permissionElements(); + Enumeration enum_ = grantEntry.permissionElements(); while (enum_.hasMoreElements()) { - PermissionNode pe = enum_.nextElement(); + PermissionEntry pe = enum_.nextElement(); expandPermissionName(pe); try { Optional perm = getInstance(pe.permission, pe.name, pe.action); @@ -136,7 +137,7 @@ private void addGrantNode(GrantNode grantEntry, PolicyInfo newInfo) throws Polic newInfo.policyEntries.add(entry); } - private void expandPermissionName(PermissionNode pe) { + private void expandPermissionName(PermissionEntry pe) { if (pe.name == null || !pe.name.contains("${{")) { return; } diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java index 3da39e18bd0a6..136b919490128 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java @@ -16,11 +16,12 @@ import java.io.Reader; import java.io.StreamTokenizer; import java.util.Enumeration; +import java.util.Optional; import java.util.Vector; public class PolicyParser { - private final Vector grantEntries = new Vector<>(); + private final Vector grantEntries = new Vector<>(); private TokenStream tokenStream; public PolicyParser() {} @@ -34,11 +35,7 @@ public void read(Reader policy) throws ParsingException, IOException { while (!tokenStream.isEOF()) { if (peek("grant")) { - GrantNode grantNode = parseGrantEntry(); - - if (grantNode != null) { - addGrantNode(grantNode); - } + parseGrantEntry().ifPresent(this::addGrantEntry); } } } @@ -81,23 +78,24 @@ private String poll(String expected) throws IOException, ParsingException { throw new ParsingException(token.line, expected, token.text); } - private GrantNode parseGrantEntry() throws ParsingException, IOException { - GrantNode grantNode = new GrantNode(); + private Optional parseGrantEntry() throws ParsingException, IOException { + GrantEntry grantEntry = new GrantEntry(); poll("grant"); while (!peek("{")) { if (pollOnMatch("Codebase")) { - if (grantNode.codeBase != null) { + if (grantEntry.codeBase != null) { throw new ParsingException(tokenStream.line(), "Multiple Codebase expressions"); } String rawCodebase = poll(tokenStream.peek().text); try { - grantNode.codeBase = PropertyExpander.expand(rawCodebase, true).replace(File.separatorChar, '/'); + grantEntry.codeBase = PropertyExpander.expand(rawCodebase, true).replace(File.separatorChar, '/'); } catch (ExpandException e) { // skip this grant as expansion failed due to missing expansion property. skipCurrentGrantBlock(); - return null; + + return Optional.empty(); } pollOnMatch(","); } else { @@ -109,8 +107,8 @@ private GrantNode parseGrantEntry() throws ParsingException, IOException { while (!peek("}")) { if (peek("Permission")) { - PermissionNode permissionEntry = parsePermissionEntry(); - grantNode.add(permissionEntry); + PermissionEntry permissionEntry = parsePermissionEntry(); + grantEntry.add(permissionEntry); poll(";"); } else { throw new ParsingException(tokenStream.line(), "Expected permission entry"); @@ -123,11 +121,11 @@ private GrantNode parseGrantEntry() throws ParsingException, IOException { poll(";"); } - if (grantNode.codeBase != null) { - grantNode.codeBase = grantNode.codeBase.replace(File.separatorChar, '/'); + if (grantEntry.codeBase != null) { + grantEntry.codeBase = grantEntry.codeBase.replace(File.separatorChar, '/'); } - return grantNode; + return Optional.of(grantEntry); } private void skipCurrentGrantBlock() throws IOException, ParsingException { @@ -161,8 +159,8 @@ private void skipCurrentGrantBlock() throws IOException, ParsingException { } } - private PermissionNode parsePermissionEntry() throws ParsingException, IOException { - PermissionNode permissionEntry = new PermissionNode(); + private PermissionEntry parsePermissionEntry() throws ParsingException, IOException { + PermissionEntry permissionEntry = new PermissionEntry(); poll("Permission"); permissionEntry.permission = poll(tokenStream.peek().text); @@ -185,11 +183,11 @@ private boolean isQuotedToken(Token token) { return token.type == '"' || token.type == '\''; } - public void addGrantNode(GrantNode grantNode) { - grantEntries.addElement(grantNode); + public void addGrantEntry(GrantEntry grantEntry) { + grantEntries.addElement(grantEntry); } - public Enumeration grantElements() { + public Enumeration grantElements() { return grantEntries.elements(); } From 0942b10c864473aa6d5c881301e0305b63f5974b Mon Sep 17 00:00:00 2001 From: Gulshan Kumar Date: Fri, 4 Apr 2025 16:34:35 +0000 Subject: [PATCH 7/8] Remove usage of enumerations --- .../org/opensearch/secure_sm/policy/GrantEntry.java | 7 +++---- .../org/opensearch/secure_sm/policy/PolicyFile.java | 10 +++------- .../org/opensearch/secure_sm/policy/PolicyParser.java | 7 ++++--- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantEntry.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantEntry.java index 3e0100c9db72b..aaf00f05ce637 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantEntry.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/GrantEntry.java @@ -8,9 +8,8 @@ package org.opensearch.secure_sm.policy; import java.io.PrintWriter; -import java.util.Collections; -import java.util.Enumeration; import java.util.LinkedList; +import java.util.List; public class GrantEntry { public String codeBase; @@ -20,8 +19,8 @@ public void add(PermissionEntry entry) { permissionEntries.add(entry); } - public Enumeration permissionElements() { - return Collections.enumeration(permissionEntries); + public List permissionElements() { + return permissionEntries; } public void write(PrintWriter out) { diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java index ed4e4679ea5bb..16822105dc145 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java @@ -29,7 +29,6 @@ import java.security.SecurityPermission; import java.security.cert.Certificate; import java.util.ArrayList; -import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Optional; @@ -77,7 +76,7 @@ private void init(URL policy, PolicyInfo newInfo) throws PolicyInitializationExc PolicyParser policyParser = new PolicyParser(); policyParser.read(reader); - for (GrantEntry grantEntry : Collections.list(policyParser.grantElements())) { + for (GrantEntry grantEntry : policyParser.grantElements()) { addGrantEntry(grantEntry, newInfo); } @@ -113,9 +112,8 @@ private void addGrantEntry(GrantEntry grantEntry, PolicyInfo newInfo) throws Pol } PolicyEntry entry = new PolicyEntry(codesource); - Enumeration enum_ = grantEntry.permissionElements(); - while (enum_.hasMoreElements()) { - PermissionEntry pe = enum_.nextElement(); + List permissionList = grantEntry.permissionElements(); + for (PermissionEntry pe : permissionList) { expandPermissionName(pe); try { Optional perm = getInstance(pe.permission, pe.name, pe.action); @@ -123,14 +121,12 @@ private void addGrantEntry(GrantEntry grantEntry, PolicyInfo newInfo) throws Pol entry.add(perm.get()); } } catch (ClassNotFoundException e) { - // these were mostly custom permission classes added for security // manager. Since security manager is deprecated, we can skip these // permissions classes. if (PERM_CLASSES_TO_SKIP.contains(pe.permission)) { continue; // skip this permission } - throw new PolicyInitializationException("Permission class not found: " + pe.permission, e); } } diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java index 136b919490128..85d71e0b92a77 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java @@ -15,7 +15,8 @@ import java.io.IOException; import java.io.Reader; import java.io.StreamTokenizer; -import java.util.Enumeration; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.Vector; @@ -187,8 +188,8 @@ public void addGrantEntry(GrantEntry grantEntry) { grantEntries.addElement(grantEntry); } - public Enumeration grantElements() { - return grantEntries.elements(); + public List grantElements() { + return Collections.unmodifiableList(grantEntries); } public static class ParsingException extends Exception { From 3091cba261c422b7ce19ef2813c089bb9901f2e8 Mon Sep 17 00:00:00 2001 From: Gulshan Kumar Date: Mon, 7 Apr 2025 09:51:50 +0000 Subject: [PATCH 8/8] Cache protection domain --- .../secure_sm/policy/PolicyFile.java | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java index 16822105dc145..e204d3dcb9053 100644 --- a/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java +++ b/libs/agent-sm/agent-policy/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java @@ -17,6 +17,8 @@ import java.net.MalformedURLException; import java.net.NetPermission; import java.net.SocketPermission; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -31,9 +33,12 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.PropertyPermission; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; @SuppressWarnings("removal") public class PolicyFile extends java.security.Policy { @@ -47,7 +52,6 @@ public class PolicyFile extends java.security.Policy { "javax.security.auth.kerberos.ServicePermission" ); - private static final int DEFAULT_CACHE_SIZE = 1; private volatile PolicyInfo policyInfo; private URL url; @@ -61,8 +65,7 @@ public PolicyFile(URL url) { } private void init(URL url) throws PolicyInitializationException { - int numCaches = DEFAULT_CACHE_SIZE; - PolicyInfo newInfo = new PolicyInfo(numCaches); + PolicyInfo newInfo = new PolicyInfo(); initPolicyFile(newInfo, url); policyInfo = newInfo; } @@ -81,7 +84,7 @@ private void init(URL policy, PolicyInfo newInfo) throws PolicyInitializationExc } } catch (Exception e) { - throw new PolicyInitializationException("Failed to load policy from: " + policy, e); + throw new PolicyInitializationException("Failed to load policy from : " + policy, e); } } @@ -191,7 +194,11 @@ public void refresh() { @Override public boolean implies(ProtectionDomain pd, Permission p) { - PermissionCollection pc = getPermissions(pd); + if (pd == null || p == null) { + return false; + } + + PermissionCollection pc = policyInfo.getOrCompute(pd, () -> getPermissions(pd)); return pc != null && pc.implies(p); } @@ -335,14 +342,20 @@ public String toString() { private static class PolicyInfo { final List policyEntries; + public final Map pdMapping; - PolicyInfo(int numCaches) { + PolicyInfo() { policyEntries = new ArrayList<>(); + pdMapping = new ConcurrentHashMap<>(); + } + + public PermissionCollection getOrCompute(ProtectionDomain pd, Supplier computeFn) { + return pdMapping.computeIfAbsent(pd, k -> computeFn.get()); } + } - @SuppressWarnings("deprecation") - private static URL newURL(String spec) throws MalformedURLException { - return new URL(spec); + private static URL newURL(String spec) throws MalformedURLException, URISyntaxException { + return new URI(spec).toURL(); } }