| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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. |
| */ |
| package org.apache.kafka.common.security; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.security.auth.login.AppConfigurationEntry; |
| import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; |
| import javax.security.auth.login.Configuration; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.fail; |
| |
| import org.apache.kafka.common.config.SaslConfigs; |
| import org.apache.kafka.common.config.types.Password; |
| import org.apache.kafka.common.network.ListenerName; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| /** |
| * Tests parsing of {@link SaslConfigs#SASL_JAAS_CONFIG} property and verifies that the format |
| * and parsing are consistent with JAAS configuration files loaded by the JRE. |
| */ |
| public class JaasContextTest { |
| |
| private File jaasConfigFile; |
| |
| @Before |
| public void setUp() throws IOException { |
| jaasConfigFile = File.createTempFile("jaas", ".conf"); |
| jaasConfigFile.deleteOnExit(); |
| System.setProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM, jaasConfigFile.toString()); |
| Configuration.setConfiguration(null); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| Files.delete(jaasConfigFile.toPath()); |
| } |
| |
| @Test |
| public void testConfigNoOptions() throws Exception { |
| checkConfiguration("test.testConfigNoOptions", LoginModuleControlFlag.REQUIRED, new HashMap<String, Object>()); |
| } |
| |
| @Test |
| public void testControlFlag() throws Exception { |
| LoginModuleControlFlag[] controlFlags = new LoginModuleControlFlag[] { |
| LoginModuleControlFlag.REQUIRED, |
| LoginModuleControlFlag.REQUISITE, |
| LoginModuleControlFlag.SUFFICIENT, |
| LoginModuleControlFlag.OPTIONAL |
| }; |
| Map<String, Object> options = new HashMap<>(); |
| options.put("propName", "propValue"); |
| for (LoginModuleControlFlag controlFlag : controlFlags) { |
| checkConfiguration("test.testControlFlag", controlFlag, options); |
| } |
| } |
| |
| @Test |
| public void testSingleOption() throws Exception { |
| Map<String, Object> options = new HashMap<>(); |
| options.put("propName", "propValue"); |
| checkConfiguration("test.testSingleOption", LoginModuleControlFlag.REQUISITE, options); |
| } |
| |
| @Test |
| public void testMultipleOptions() throws Exception { |
| Map<String, Object> options = new HashMap<>(); |
| for (int i = 0; i < 10; i++) |
| options.put("propName" + i, "propValue" + i); |
| checkConfiguration("test.testMultipleOptions", LoginModuleControlFlag.SUFFICIENT, options); |
| } |
| |
| @Test |
| public void testQuotedOptionValue() throws Exception { |
| Map<String, Object> options = new HashMap<>(); |
| options.put("propName", "prop value"); |
| options.put("propName2", "value1 = 1, value2 = 2"); |
| String config = String.format("test.testQuotedOptionValue required propName=\"%s\" propName2=\"%s\";", options.get("propName"), options.get("propName2")); |
| checkConfiguration(config, "test.testQuotedOptionValue", LoginModuleControlFlag.REQUIRED, options); |
| } |
| |
| @Test |
| public void testQuotedOptionName() throws Exception { |
| Map<String, Object> options = new HashMap<>(); |
| options.put("prop name", "propValue"); |
| String config = "test.testQuotedOptionName required \"prop name\"=propValue;"; |
| checkConfiguration(config, "test.testQuotedOptionName", LoginModuleControlFlag.REQUIRED, options); |
| } |
| |
| @Test |
| public void testMultipleLoginModules() throws Exception { |
| StringBuilder builder = new StringBuilder(); |
| int moduleCount = 3; |
| Map<Integer, Map<String, Object>> moduleOptions = new HashMap<>(); |
| for (int i = 0; i < moduleCount; i++) { |
| Map<String, Object> options = new HashMap<>(); |
| options.put("index", "Index" + i); |
| options.put("module", "Module" + i); |
| moduleOptions.put(i, options); |
| String module = jaasConfigProp("test.Module" + i, LoginModuleControlFlag.REQUIRED, options); |
| builder.append(' '); |
| builder.append(module); |
| } |
| String jaasConfigProp = builder.toString(); |
| |
| String clientContextName = "CLIENT"; |
| Configuration configuration = new JaasConfig(clientContextName, jaasConfigProp); |
| AppConfigurationEntry[] dynamicEntries = configuration.getAppConfigurationEntry(clientContextName); |
| assertEquals(moduleCount, dynamicEntries.length); |
| |
| for (int i = 0; i < moduleCount; i++) { |
| AppConfigurationEntry entry = dynamicEntries[i]; |
| checkEntry(entry, "test.Module" + i, LoginModuleControlFlag.REQUIRED, moduleOptions.get(i)); |
| } |
| |
| String serverContextName = "SERVER"; |
| writeConfiguration(serverContextName, jaasConfigProp); |
| AppConfigurationEntry[] staticEntries = Configuration.getConfiguration().getAppConfigurationEntry(serverContextName); |
| for (int i = 0; i < moduleCount; i++) { |
| AppConfigurationEntry staticEntry = staticEntries[i]; |
| checkEntry(staticEntry, dynamicEntries[i].getLoginModuleName(), LoginModuleControlFlag.REQUIRED, dynamicEntries[i].getOptions()); |
| } |
| } |
| |
| @Test |
| public void testMissingLoginModule() throws Exception { |
| checkInvalidConfiguration(" required option1=value1;"); |
| } |
| |
| @Test |
| public void testMissingControlFlag() throws Exception { |
| checkInvalidConfiguration("test.loginModule option1=value1;"); |
| } |
| |
| @Test |
| public void testMissingOptionValue() throws Exception { |
| checkInvalidConfiguration("loginModule required option1;"); |
| } |
| |
| @Test |
| public void testMissingSemicolon() throws Exception { |
| checkInvalidConfiguration("test.testMissingSemicolon required option1=value1"); |
| } |
| |
| @Test |
| public void testNumericOptionWithoutQuotes() throws Exception { |
| checkInvalidConfiguration("test.testNumericOptionWithoutQuotes required option1=3;"); |
| } |
| |
| @Test |
| public void testInvalidControlFlag() throws Exception { |
| checkInvalidConfiguration("test.testInvalidControlFlag { option1=3;"); |
| } |
| |
| @Test |
| public void testNumericOptionWithQuotes() throws Exception { |
| Map<String, Object> options = new HashMap<>(); |
| options.put("option1", "3"); |
| String config = "test.testNumericOptionWithQuotes required option1=\"3\";"; |
| checkConfiguration(config, "test.testNumericOptionWithQuotes", LoginModuleControlFlag.REQUIRED, options); |
| } |
| |
| @Test |
| public void testLoadForServerWithListenerNameOverride() throws IOException { |
| writeConfiguration(Arrays.asList( |
| "KafkaServer { test.LoginModuleDefault required; };", |
| "plaintext.KafkaServer { test.LoginModuleOverride requisite; };" |
| )); |
| JaasContext context = JaasContext.loadServerContext(new ListenerName("plaintext"), |
| "SOME-MECHANISM", Collections.emptyMap()); |
| assertEquals("plaintext.KafkaServer", context.name()); |
| assertEquals(JaasContext.Type.SERVER, context.type()); |
| assertEquals(1, context.configurationEntries().size()); |
| checkEntry(context.configurationEntries().get(0), "test.LoginModuleOverride", |
| LoginModuleControlFlag.REQUISITE, Collections.emptyMap()); |
| } |
| |
| @Test |
| public void testLoadForServerWithListenerNameAndFallback() throws IOException { |
| writeConfiguration(Arrays.asList( |
| "KafkaServer { test.LoginModule required; };", |
| "other.KafkaServer { test.LoginModuleOther requisite; };" |
| )); |
| JaasContext context = JaasContext.loadServerContext(new ListenerName("plaintext"), |
| "SOME-MECHANISM", Collections.emptyMap()); |
| assertEquals("KafkaServer", context.name()); |
| assertEquals(JaasContext.Type.SERVER, context.type()); |
| assertEquals(1, context.configurationEntries().size()); |
| checkEntry(context.configurationEntries().get(0), "test.LoginModule", LoginModuleControlFlag.REQUIRED, |
| Collections.emptyMap()); |
| } |
| |
| @Test(expected = IllegalArgumentException.class) |
| public void testLoadForServerWithWrongListenerName() throws IOException { |
| writeConfiguration("Server", "test.LoginModule required;"); |
| JaasContext.loadServerContext(new ListenerName("plaintext"), "SOME-MECHANISM", |
| Collections.emptyMap()); |
| } |
| |
| private AppConfigurationEntry configurationEntry(JaasContext.Type contextType, String jaasConfigProp) { |
| Password saslJaasConfig = jaasConfigProp == null ? null : new Password(jaasConfigProp); |
| JaasContext context = JaasContext.load(contextType, null, contextType.name(), saslJaasConfig); |
| List<AppConfigurationEntry> entries = context.configurationEntries(); |
| assertEquals(1, entries.size()); |
| return entries.get(0); |
| } |
| |
| private String controlFlag(LoginModuleControlFlag loginModuleControlFlag) { |
| // LoginModuleControlFlag.toString() has format "LoginModuleControlFlag: flag" |
| String[] tokens = loginModuleControlFlag.toString().split(" "); |
| return tokens[tokens.length - 1]; |
| } |
| |
| private String jaasConfigProp(String loginModule, LoginModuleControlFlag controlFlag, Map<String, Object> options) { |
| StringBuilder builder = new StringBuilder(); |
| builder.append(loginModule); |
| builder.append(' '); |
| builder.append(controlFlag(controlFlag)); |
| for (Map.Entry<String, Object> entry : options.entrySet()) { |
| builder.append(' '); |
| builder.append(entry.getKey()); |
| builder.append('='); |
| builder.append(entry.getValue()); |
| } |
| builder.append(';'); |
| return builder.toString(); |
| } |
| |
| private void writeConfiguration(String contextName, String jaasConfigProp) throws IOException { |
| List<String> lines = Arrays.asList(contextName + " { ", jaasConfigProp, "};"); |
| writeConfiguration(lines); |
| } |
| |
| private void writeConfiguration(List<String> lines) throws IOException { |
| Files.write(jaasConfigFile.toPath(), lines, StandardCharsets.UTF_8); |
| Configuration.setConfiguration(null); |
| } |
| |
| private void checkConfiguration(String loginModule, LoginModuleControlFlag controlFlag, Map<String, Object> options) throws Exception { |
| String jaasConfigProp = jaasConfigProp(loginModule, controlFlag, options); |
| checkConfiguration(jaasConfigProp, loginModule, controlFlag, options); |
| } |
| |
| private void checkEntry(AppConfigurationEntry entry, String loginModule, LoginModuleControlFlag controlFlag, Map<String, ?> options) { |
| assertEquals(loginModule, entry.getLoginModuleName()); |
| assertEquals(controlFlag, entry.getControlFlag()); |
| assertEquals(options, entry.getOptions()); |
| } |
| |
| private void checkConfiguration(String jaasConfigProp, String loginModule, LoginModuleControlFlag controlFlag, Map<String, Object> options) throws Exception { |
| AppConfigurationEntry dynamicEntry = configurationEntry(JaasContext.Type.CLIENT, jaasConfigProp); |
| checkEntry(dynamicEntry, loginModule, controlFlag, options); |
| assertNull("Static configuration updated", Configuration.getConfiguration().getAppConfigurationEntry(JaasContext.Type.CLIENT.name())); |
| |
| writeConfiguration(JaasContext.Type.SERVER.name(), jaasConfigProp); |
| AppConfigurationEntry staticEntry = configurationEntry(JaasContext.Type.SERVER, null); |
| checkEntry(staticEntry, loginModule, controlFlag, options); |
| } |
| |
| private void checkInvalidConfiguration(String jaasConfigProp) throws IOException { |
| try { |
| writeConfiguration(JaasContext.Type.SERVER.name(), jaasConfigProp); |
| AppConfigurationEntry entry = configurationEntry(JaasContext.Type.SERVER, null); |
| fail("Invalid JAAS configuration file didn't throw exception, entry=" + entry); |
| } catch (SecurityException e) { |
| // Expected exception |
| } |
| try { |
| AppConfigurationEntry entry = configurationEntry(JaasContext.Type.CLIENT, jaasConfigProp); |
| fail("Invalid JAAS configuration property didn't throw exception, entry=" + entry); |
| } catch (IllegalArgumentException e) { |
| // Expected exception |
| } |
| } |
| } |