From 4688fd1333133a729e064d993f9ea2f21ec5f408 Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Wed, 27 Nov 2024 13:53:28 +0200 Subject: [PATCH] [Security Manager Replacement] Native Java Agent (dynamic code rewriting, must be low overhead) Signed-off-by: Andriy Redko --- buildSrc/build.gradle | 4 +- .../gradle/OpenSearchTestBasePlugin.java | 3 +- .../src/main/resources/minimumRuntimeVersion | 2 +- distribution/src/config/jvm.options | 4 +- .../tools/launchers/SystemJvmOptions.java | 2 +- libs/agent-sm/agent/build.gradle | 44 + .../licenses/byte-buddy-1.15.10.jar.sha1 | 1 + .../agent/licenses/byte-buddy-LICENSE.txt | 180 ++ .../agent/licenses/byte-buddy-NOTICE.txt | 13 + .../java/org/opensearch/javaagent/Agent.java | 90 + .../javaagent/SocketChannelInterceptor.java | 54 + .../javaagent/StackCallerChainExtractor.java | 35 + .../opensearch/javaagent/package-info.java | 12 + libs/agent-sm/bootstrap/build.gradle | 22 + .../javaagent/bootstrap/AgentPolicy.java | 56 + .../javaagent/bootstrap/package-info.java | 12 + libs/agent-sm/build.gradle | 19 + libs/build.gradle | 26 +- .../secure_sm/policy/ParseUtil.java | 588 ++++++ .../opensearch/secure_sm/policy/Password.java | 145 ++ .../secure_sm/policy/PolicyFile.java | 1573 +++++++++++++++++ .../secure_sm/policy/PolicyParser.java | 1135 ++++++++++++ .../secure_sm/policy/PolicyUtil.java | 142 ++ .../secure_sm/policy/PropertyExpander.java | 105 ++ .../secure_sm/policy/SecurityConstants.java | 117 ++ .../cache/store/disk/EhcacheDiskCache.java | 3 +- .../telemetry/OTelTelemetrySettings.java | 2 +- .../metrics/OTelMetricsTelemetry.java | 3 + .../tracing/sampler/OTelSamplerFactory.java | 1 + server/build.gradle | 2 +- .../opensearch/bootstrap/BootstrapChecks.java | 8 +- .../org/opensearch/bootstrap/OpenSearch.java | 13 - .../org/opensearch/bootstrap/Security.java | 12 +- .../common/util/concurrent/ThreadContext.java | 19 - .../store/remote/utils/TransferManager.java | 1 + 35 files changed, 4386 insertions(+), 62 deletions(-) create mode 100644 libs/agent-sm/agent/build.gradle create mode 100644 libs/agent-sm/agent/licenses/byte-buddy-1.15.10.jar.sha1 create mode 100644 libs/agent-sm/agent/licenses/byte-buddy-LICENSE.txt create mode 100644 libs/agent-sm/agent/licenses/byte-buddy-NOTICE.txt create mode 100644 libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/Agent.java create mode 100644 libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SocketChannelInterceptor.java create mode 100644 libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/StackCallerChainExtractor.java create mode 100644 libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/package-info.java create mode 100644 libs/agent-sm/bootstrap/build.gradle create mode 100644 libs/agent-sm/bootstrap/src/main/java/org/opensearch/javaagent/bootstrap/AgentPolicy.java create mode 100644 libs/agent-sm/bootstrap/src/main/java/org/opensearch/javaagent/bootstrap/package-info.java create mode 100644 libs/agent-sm/build.gradle create mode 100644 libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java create mode 100644 libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/Password.java create mode 100644 libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java create mode 100644 libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java create mode 100644 libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java create mode 100644 libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java create mode 100644 libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index f7fc0d7760993..55b6654fb4cfe 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -171,8 +171,8 @@ if (project != rootProject) { allprojects { java { - targetCompatibility = JavaVersion.VERSION_11 - sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_17 } } diff --git a/buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java b/buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java index d0cb2da9c1dd3..617ad616de9ac 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/resources/minimumRuntimeVersion b/buildSrc/src/main/resources/minimumRuntimeVersion index b4de394767536..98d9bcb75a685 100644 --- a/buildSrc/src/main/resources/minimumRuntimeVersion +++ b/buildSrc/src/main/resources/minimumRuntimeVersion @@ -1 +1 @@ -11 +17 diff --git a/distribution/src/config/jvm.options b/distribution/src/config/jvm.options index a8c96f33ce51d..7805abd4fdf8d 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,5 @@ ${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 + +24:-javaagent:agent/opensearch-agent-3.0.0-SNAPSHOT.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/libs/agent-sm/agent/build.gradle b/libs/agent-sm/agent/build.gradle new file mode 100644 index 0000000000000..ae3958fc756d3 --- /dev/null +++ b/libs/agent-sm/agent/build.gradle @@ -0,0 +1,44 @@ + +apply plugin: 'opensearch.build' +apply plugin: 'opensearch.publish' + +base { + archivesName = 'opensearch-agent' +} + +configurations { + bootstrapConfiguration.extendsFrom(implementation) +} + +dependencies { + implementation project(":libs:agent-sm:bootstrap") + implementation "net.bytebuddy:byte-buddy:1.15.10" + compileOnly "com.google.code.findbugs:jsr305:3.0.2" +} + +jar { + manifest { + attributes( + "Can-Redefine-Classes": "true", + "Can-Retransform-Classes": "true", + "Agent-Class": "org.opensearch.javaagent.Agent", + "Premain-Class": "org.opensearch.javaagent.Agent", + "Boot-Class-Path": 'byte-buddy-1.15.10.jar opensearch-agent-bootstrap-3.0.0-SNAPSHOT.jar' + ) + } +} + +compileJava { + options.compilerArgs -= '-Werror' +} + +test.enabled = false + +tasks.named('forbiddenApisMain').configure { + replaceSignatureFiles 'jdk-signatures' +} + +copy { + from configurations.runtimeClasspath + into "$buildDir/distributions" +} diff --git a/libs/agent-sm/agent/licenses/byte-buddy-1.15.10.jar.sha1 b/libs/agent-sm/agent/licenses/byte-buddy-1.15.10.jar.sha1 new file mode 100644 index 0000000000000..b89163a2aa842 --- /dev/null +++ b/libs/agent-sm/agent/licenses/byte-buddy-1.15.10.jar.sha1 @@ -0,0 +1 @@ +635c873fadd853c084f84fdc3cbd58c5dd8537f9 \ No newline at end of file diff --git a/libs/agent-sm/agent/licenses/byte-buddy-LICENSE.txt b/libs/agent-sm/agent/licenses/byte-buddy-LICENSE.txt new file mode 100644 index 0000000000000..719c6605bb9b4 --- /dev/null +++ b/libs/agent-sm/agent/licenses/byte-buddy-LICENSE.txt @@ -0,0 +1,180 @@ +This product bundles ASM 9.7.1, which is available under a "3-clause BSD" +license. For details, see licenses/ASM. For more information visit ${asm.url}. + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/libs/agent-sm/agent/licenses/byte-buddy-NOTICE.txt b/libs/agent-sm/agent/licenses/byte-buddy-NOTICE.txt new file mode 100644 index 0000000000000..929cbc6b18bba --- /dev/null +++ b/libs/agent-sm/agent/licenses/byte-buddy-NOTICE.txt @@ -0,0 +1,13 @@ +Copyright 2014 - Present Rafael Winterhalter + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/Agent.java b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/Agent.java new file mode 100644 index 0000000000000..85705a02cb307 --- /dev/null +++ b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/Agent.java @@ -0,0 +1,90 @@ +/* + * 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.javaagent; + +import org.opensearch.javaagent.bootstrap.AgentPolicy; + +import java.lang.instrument.Instrumentation; +import java.nio.channels.SocketChannel; +import java.util.Map; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.dynamic.loading.ClassInjector; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.matcher.ElementMatcher.Junction; +import net.bytebuddy.matcher.ElementMatchers; + +/** + * Java Agent + */ +public class Agent { + /** + * Constructor + */ + private Agent() {} + + /** + * Premain + * @param agentArguments agent arguments + * @param instrumentation instrumentation + * @throws Exception Exception + */ + public static void premain(String agentArguments, Instrumentation instrumentation) throws Exception { + initAgent(instrumentation); + } + + /** + * Agent Main + * @param agentArguments agent arguments + * @param instrumentation instrumentation + * @throws Exception Exception + */ + public static void agentmain(String agentArguments, Instrumentation instrumentation) throws Exception { + initAgent(instrumentation); + } + + private static AgentBuilder createAgentBuilder(Instrumentation inst) throws Exception { + final Junction systemType = ElementMatchers.isSubTypeOf(SocketChannel.class); + + final AgentBuilder.Transformer transformer = (b, typeDescription, classLoader, module, pd) -> b.visit( + Advice.to(SocketChannelInterceptor.class) + .on(ElementMatchers.named("connect").and(ElementMatchers.not(ElementMatchers.isAbstract()))) + ); + + ClassInjector.UsingUnsafe.ofBootLoader() + .inject( + Map.of( + new TypeDescription.ForLoadedType(StackCallerChainExtractor.class), + ClassFileLocator.ForClassLoader.read(StackCallerChainExtractor.class), + new TypeDescription.ForLoadedType(AgentPolicy.class), + ClassFileLocator.ForClassLoader.read(AgentPolicy.class) + ) + ); + + final ByteBuddy byteBuddy = new ByteBuddy().with(Implementation.Context.Disabled.Factory.INSTANCE); + final AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) + .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) + .with(AgentBuilder.RedefinitionStrategy.Listener.StreamWriting.toSystemError()) + .with(AgentBuilder.TypeStrategy.Default.REDEFINE) + .ignore(ElementMatchers.none()) + .type(systemType) + .transform(transformer); + + return agentBuilder; + } + + private static void initAgent(Instrumentation instrumentation) throws Exception { + AgentBuilder agentBuilder = createAgentBuilder(instrumentation); + agentBuilder.installOn(instrumentation); + } +} diff --git a/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SocketChannelInterceptor.java b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SocketChannelInterceptor.java new file mode 100644 index 0000000000000..f07e5d4ae004b --- /dev/null +++ b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/SocketChannelInterceptor.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.javaagent; + +import org.opensearch.javaagent.bootstrap.AgentPolicy; + +import java.lang.StackWalker.Option; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.SocketPermission; +import java.security.Policy; +import java.security.ProtectionDomain; +import java.util.List; + +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Origin; + +/** + * SocketChannelInterceptor + */ +public class SocketChannelInterceptor { + /** + * SocketChannelInterceptor + */ + public SocketChannelInterceptor() {} + + /** + * Interceptors + * @param args arguments + * @param method method + * @throws Exception exceptions + */ + @Advice.OnMethodEnter + public static void intercept(@Advice.AllArguments Object[] args, @Origin Method method) throws Exception { + final Policy policy = AgentPolicy.getPolicy(); + final StackWalker walker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE); + + final Class caller = walker.getCallerClass(); + final List callers = walker.walk(new StackCallerChainExtractor()); + + final InetSocketAddress address = (InetSocketAddress) args[0]; + for (final ProtectionDomain domain : callers) { + if (!policy.implies(domain, new SocketPermission(address.getHostName() + ":" + address.getPort(), "connect,resolve"))) { + throw new SecurityException("Denied access to " + address); + } + } + } +} diff --git a/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/StackCallerChainExtractor.java b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/StackCallerChainExtractor.java new file mode 100644 index 0000000000000..e1a15c33684bb --- /dev/null +++ b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/StackCallerChainExtractor.java @@ -0,0 +1,35 @@ +/* + * 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.javaagent; + +import java.lang.StackWalker.StackFrame; +import java.security.ProtectionDomain; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Stack Caller Chain Extractor + */ +public final class StackCallerChainExtractor implements Function, List> { + /** + * Constructor + */ + public StackCallerChainExtractor() {} + + /** + * Folds the stack + * @param frames stack frames + */ + @Override + public List apply(Stream frames) { + return frames.map(StackFrame::getDeclaringClass).map(Class::getProtectionDomain).distinct().collect(Collectors.toList()); + } +} diff --git a/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/package-info.java b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/package-info.java new file mode 100644 index 0000000000000..447a0d8828875 --- /dev/null +++ b/libs/agent-sm/agent/src/main/java/org/opensearch/javaagent/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 + */ +package org.opensearch.javaagent; diff --git a/libs/agent-sm/bootstrap/build.gradle b/libs/agent-sm/bootstrap/build.gradle new file mode 100644 index 0000000000000..c41e4825f9f9f --- /dev/null +++ b/libs/agent-sm/bootstrap/build.gradle @@ -0,0 +1,22 @@ +/* + * 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. + */ + +// This file is intentionally blank. All configuration of the +// distribution is done in the parent project. + +// See please https://docs.gradle.org/8.5/userguide/upgrading_version_8.html#deprecated_missing_project_directory + +apply plugin: 'opensearch.build' +apply plugin: 'opensearch.publish' + +base { + archivesName = 'opensearch-agent-bootstrap' +} diff --git a/libs/agent-sm/bootstrap/src/main/java/org/opensearch/javaagent/bootstrap/AgentPolicy.java b/libs/agent-sm/bootstrap/src/main/java/org/opensearch/javaagent/bootstrap/AgentPolicy.java new file mode 100644 index 0000000000000..44a9273cfe754 --- /dev/null +++ b/libs/agent-sm/bootstrap/src/main/java/org/opensearch/javaagent/bootstrap/AgentPolicy.java @@ -0,0 +1,56 @@ +package org.opensearch.javaagent.bootstrap; + +import java.lang.StackWalker.Option; +import java.lang.StackWalker.StackFrame; +import java.security.Permission; +import java.security.Policy; +import java.security.ProtectionDomain; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Agent Policy + */ +@SuppressWarnings("removal") +public class AgentPolicy { + private static volatile Policy policy; + + private AgentPolicy() {} + + /** + * Set Agent policy + * @param policy policy + */ + public static void setPolicy(Policy policy) { + if (AgentPolicy.policy == null) { + AgentPolicy.policy = policy; + } else { + throw new SecurityException("The Policy has been set already: " + AgentPolicy.policy); + } + } + + /** + * Check permissions + * @param permission permission + */ + public static void checkPermission(Permission permission) { + final StackWalker walker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE); + final List callers = walker.walk( + frames -> frames.map(StackFrame::getDeclaringClass).map(Class::getProtectionDomain).distinct().collect(Collectors.toList()) + ); + + for (final ProtectionDomain domain : callers) { + if (!policy.implies(domain, permission)) { + throw new SecurityException("Denied access: " + permission); + } + } + } + + /** + * Get policy + * @return policy + */ + public static Policy getPolicy() { + return policy; + } +} diff --git a/libs/agent-sm/bootstrap/src/main/java/org/opensearch/javaagent/bootstrap/package-info.java b/libs/agent-sm/bootstrap/src/main/java/org/opensearch/javaagent/bootstrap/package-info.java new file mode 100644 index 0000000000000..6172ae511a8f7 --- /dev/null +++ b/libs/agent-sm/bootstrap/src/main/java/org/opensearch/javaagent/bootstrap/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.javaagent.bootstrap; diff --git a/libs/agent-sm/build.gradle b/libs/agent-sm/build.gradle new file mode 100644 index 0000000000000..3f8caaa6e9967 --- /dev/null +++ b/libs/agent-sm/build.gradle @@ -0,0 +1,19 @@ +/* + * 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. + */ + +// This file is intentionally blank. All configuration of the +// distribution is done in the parent project. + +// See please https://docs.gradle.org/8.5/userguide/upgrading_version_8.html#deprecated_missing_project_directory + +base { + archivesName = 'opensearch-agent-sm' +} diff --git a/libs/build.gradle b/libs/build.gradle index c0fcc1ff2b977..ac925b24eafeb 100644 --- a/libs/build.gradle +++ b/libs/build.gradle @@ -40,19 +40,21 @@ subprojects { * other libs. This keeps our dependencies simpler. */ project.afterEvaluate { - 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}") + 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}") + } + } } - } } } } diff --git a/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java new file mode 100644 index 0000000000000..254eb0a2a518c --- /dev/null +++ b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/ParseUtil.java @@ -0,0 +1,588 @@ +/* + * 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; + +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/secure-sm/src/main/java/org/opensearch/secure_sm/policy/Password.java b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/Password.java new file mode 100644 index 0000000000000..333c9c80e1898 --- /dev/null +++ b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/Password.java @@ -0,0 +1,145 @@ +/* + * 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; + +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/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java new file mode 100644 index 0000000000000..19845181ee6c1 --- /dev/null +++ b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyFile.java @@ -0,0 +1,1573 @@ +/* + * 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; + +@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/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java new file mode 100644 index 0000000000000..e2a9ce0852677 --- /dev/null +++ b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyParser.java @@ -0,0 +1,1135 @@ +/* + * 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; + +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/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java new file mode 100644 index 0000000000000..3719ee9d723e7 --- /dev/null +++ b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PolicyUtil.java @@ -0,0 +1,142 @@ +/* + * 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; + +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/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java new file mode 100644 index 0000000000000..f8d86e26ebcce --- /dev/null +++ b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/PropertyExpander.java @@ -0,0 +1,105 @@ +/* + * 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; + +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/secure-sm/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java new file mode 100644 index 0000000000000..c966c70bf0915 --- /dev/null +++ b/libs/secure-sm/src/main/java/org/opensearch/secure_sm/policy/SecurityConstants.java @@ -0,0 +1,117 @@ +/* + * 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; + +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/plugins/cache-ehcache/src/main/java/org/opensearch/cache/store/disk/EhcacheDiskCache.java b/plugins/cache-ehcache/src/main/java/org/opensearch/cache/store/disk/EhcacheDiskCache.java index 33c27eb301ad1..96a323c00d8b4 100644 --- a/plugins/cache-ehcache/src/main/java/org/opensearch/cache/store/disk/EhcacheDiskCache.java +++ b/plugins/cache-ehcache/src/main/java/org/opensearch/cache/store/disk/EhcacheDiskCache.java @@ -194,7 +194,7 @@ PersistentCacheManager getCacheManager() { return this.cacheManager; } - @SuppressWarnings({ "rawtypes" }) + @SuppressWarnings({ "rawtypes", "removal" }) private Cache buildCache(Duration expireAfterAccess, Builder builder) { // Creating the cache requires permissions specified in plugin-security.policy return AccessController.doPrivileged((PrivilegedAction>) () -> { @@ -279,6 +279,7 @@ Map, CompletableFuture, V>>> getCompletableFutur return completableFutureMap; } + @SuppressWarnings("removal") @SuppressForbidden(reason = "Ehcache uses File.io") PersistentCacheManager buildCacheManager() { // In case we use multiple ehCaches, we can define this cache manager at a global level. diff --git a/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/OTelTelemetrySettings.java b/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/OTelTelemetrySettings.java index 95ce6918fcb70..91e8268622677 100644 --- a/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/OTelTelemetrySettings.java +++ b/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/OTelTelemetrySettings.java @@ -120,7 +120,7 @@ private OTelTelemetrySettings() {} /** * Samplers orders setting. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "removal" }) public static final Setting>> OTEL_TRACER_SPAN_SAMPLER_CLASS_SETTINGS = Setting.listSetting( "telemetry.otel.tracer.span.sampler.classes", Arrays.asList(ProbabilisticTransportActionSampler.class.getName(), ProbabilisticSampler.class.getName()), diff --git a/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/metrics/OTelMetricsTelemetry.java b/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/metrics/OTelMetricsTelemetry.java index 3258e91738ba6..5923fcd3f42ea 100644 --- a/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/metrics/OTelMetricsTelemetry.java +++ b/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/metrics/OTelMetricsTelemetry.java @@ -83,6 +83,7 @@ public Counter createUpDownCounter(String name, String description, String unit) * @return histogram */ @Override + @SuppressWarnings("removal") public Histogram createHistogram(String name, String description, String unit) { DoubleHistogram doubleHistogram = AccessController.doPrivileged( (PrivilegedAction) () -> otelMeter.histogramBuilder(name).setUnit(unit).setDescription(description).build() @@ -91,6 +92,7 @@ public Histogram createHistogram(String name, String description, String unit) { } @Override + @SuppressWarnings("removal") public Closeable createGauge(String name, String description, String unit, Supplier valueProvider, Tags tags) { ObservableDoubleGauge doubleObservableGauge = AccessController.doPrivileged( (PrivilegedAction) () -> otelMeter.gaugeBuilder(name) @@ -102,6 +104,7 @@ public Closeable createGauge(String name, String description, String unit, Suppl } @Override + @SuppressWarnings("removal") public Closeable createGauge(String name, String description, String unit, Supplier value) { ObservableDoubleGauge doubleObservableGauge = AccessController.doPrivileged( (PrivilegedAction) () -> otelMeter.gaugeBuilder(name) diff --git a/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/tracing/sampler/OTelSamplerFactory.java b/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/tracing/sampler/OTelSamplerFactory.java index b9d5c07a40cd8..da8887867a43f 100644 --- a/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/tracing/sampler/OTelSamplerFactory.java +++ b/plugins/telemetry-otel/src/main/java/org/opensearch/telemetry/tracing/sampler/OTelSamplerFactory.java @@ -63,6 +63,7 @@ public static Sampler create(TelemetrySettings telemetrySettings, Settings setti return fallbackSampler; } + @SuppressWarnings("removal") private static Sampler instantiateSampler( Class samplerClassName, TelemetrySettings telemetrySettings, diff --git a/server/build.gradle b/server/build.gradle index 6559c7247200a..a1d89e7a79228 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -69,7 +69,7 @@ dependencies { api project(":libs:opensearch-geo") api project(":libs:opensearch-telemetry") api project(":libs:opensearch-task-commons") - + implementation project(":libs:agent-sm:bootstrap") compileOnly project(':libs:opensearch-plugin-classloader') testRuntimeOnly project(':libs:opensearch-plugin-classloader') diff --git a/server/src/main/java/org/opensearch/bootstrap/BootstrapChecks.java b/server/src/main/java/org/opensearch/bootstrap/BootstrapChecks.java index 485dd43a5999c..99adf016b92b4 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 8eb4f841b9671..c4426861e06e1 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 563a026109059..03ba2bfe4e141 100644 --- a/server/src/main/java/org/opensearch/bootstrap/Security.java +++ b/server/src/main/java/org/opensearch/bootstrap/Security.java @@ -40,9 +40,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; @@ -58,7 +59,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; @@ -143,7 +143,7 @@ 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( + AgentPolicy.setPolicy( new OpenSearchPolicy( codebases, createPermissions(environment), @@ -158,7 +158,7 @@ static void configure(Environment environment, boolean filterBadDefaults) throws // 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)); + // System.setSecurityManager(new SecureSM(classesThatCanExit)); // do some basic tests selfTest(); @@ -279,14 +279,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 75a7ef94978d4..1d030e6dd8901 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 @@ -45,13 +45,11 @@ 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 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; @@ -222,10 +215,6 @@ public Writeable captureAsWriteable() { * permission org.opensearch.secure_sm.ThreadContextPermission "stashWithOrigin"; */ 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; @@ -244,10 +233,6 @@ public StoredContext stashWithOrigin(String origin) { * permission org.opensearch.secure_sm.ThreadContextPermission "stashAndMergeHeaders"; */ 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); @@ -602,10 +587,6 @@ boolean isDefaultContext() { * permission org.opensearch.secure_sm.ThreadContextPermission "markAsSystemContext"; */ 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/java/org/opensearch/index/store/remote/utils/TransferManager.java b/server/src/main/java/org/opensearch/index/store/remote/utils/TransferManager.java index 77a8ccfafbac2..a80823d7b9109 100644 --- a/server/src/main/java/org/opensearch/index/store/remote/utils/TransferManager.java +++ b/server/src/main/java/org/opensearch/index/store/remote/utils/TransferManager.java @@ -67,6 +67,7 @@ public TransferManager(final StreamReader streamReader, final FileCache fileCach * @param blobFetchRequest to fetch * @return future of IndexInput augmented with internal caching maintenance tasks */ + @SuppressWarnings("removal") public IndexInput fetchBlob(BlobFetchRequest blobFetchRequest) throws IOException { final Path key = blobFetchRequest.getFilePath(); logger.trace("fetchBlob called for {}", key.toString());