├── src ├── test │ ├── resources │ │ ├── roles.yml │ │ ├── login.conf_template │ │ └── log4j.properties │ └── java │ │ ├── org │ │ └── elasticsearch │ │ │ └── node │ │ │ └── PluginEnabledNode.java │ │ └── de │ │ └── codecentric │ │ └── elasticsearch │ │ └── plugin │ │ └── kerberosrealm │ │ ├── support │ │ └── EmbeddedKRBServer.java │ │ ├── client │ │ └── MockingKerberizedClient.java │ │ ├── AbstractUnitTest.java │ │ └── KerberosRealmEmbeddedTests.java └── main │ └── java │ └── de │ └── codecentric │ └── elasticsearch │ └── plugin │ └── kerberosrealm │ ├── support │ ├── SettingConstants.java │ ├── KrbConstants.java │ ├── PropertyUtil.java │ └── JaasKrbUtil.java │ ├── realm │ ├── KerberosRealmFactory.java │ ├── KerberosAuthenticationToken.java │ ├── KerberosAuthenticationFailureHandler.java │ └── KerberosRealm.java │ ├── rest │ └── LoginInfoRestAction.java │ ├── KerberosRealmPlugin.java │ └── client │ └── KerberizedClient.java ├── .travis.yml ├── integ-test ├── start_kdc.sh ├── do_rest_request.sh └── start_elasticsearch.sh ├── NOTICE ├── .gitignore ├── integration-tests.xml ├── README.md ├── pom.xml └── LICENSE /src/test/resources/roles.yml: -------------------------------------------------------------------------------- 1 | cc_kerberos_realm_role: 2 | cluster: all 3 | indices: 4 | '*': all -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: true 3 | 4 | jdk: 5 | - oraclejdk7 6 | - oraclejdk8 7 | 8 | after_success: 9 | - mvn clean test jacoco:report coveralls:report -Pcoverage -Dmaven.build.timestamp.format=yyMMddHHmm 10 | -------------------------------------------------------------------------------- /integ-test/start_kdc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | cd $DIR/../.. 4 | git clone https://github.com/salyh/kerby-dist 5 | cd $DIR/.. 6 | rm -rf target/kdc_work/ 7 | mkdir -p target/kdc_work 8 | cd ../kerby-dist/kdc-dist-1.0.0-RC1 9 | echo "Start KDC, logs are here $(pwd)/logs" 10 | ./bin/start-kdc.sh conf/ $DIR/../target/kdc_work -------------------------------------------------------------------------------- /integ-test/do_rest_request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | export KRB5_CONFIG=$DIR/../../kerby-dist/kdc-dist-1.0.0-RC1/conf/krb5.conf 4 | echo "Password is: lukepwd" 5 | kinit luke@EXAMPLE.COM || { echo 'kinit failed' ; exit -1; } 6 | curl -vvv --negotiate -u : "http://localhost:9200/?pretty" 7 | curl -vvv --negotiate -u : "http://localhost:9200/_cluster/health?pretty" 8 | curl -vvv --negotiate -u : "http://localhost:9200/_logininfo?pretty" -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Elasticsearch Shield Kerberos Custom Realm 2 | Copyright 2015 codecentric AG 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | 7 | - Apache Tomcat is licensed under Apache License v2.0. 8 | - Apache Kerby is licensed under Apache License v2.0. 9 | 10 | "Elasticsearch" is licensed under Apache License v2.0. 11 | You can obtain a copy of the License at: https://github.com/elastic/elasticsearch/blob/master/LICENSE.txt -------------------------------------------------------------------------------- /src/test/resources/login.conf_template: -------------------------------------------------------------------------------- 1 | com.sun.security.jgss.krb5.initiate { 2 | com.sun.security.auth.module.Krb5LoginModule 3 | required 4 | refreshKrb5Config=true 5 | storeKey=false 6 | principal="${initiator.principal}" 7 | ticketCache="${initiator.ticketcache}" 8 | useTicketCache=true 9 | debug=${debug} 10 | ; 11 | }; 12 | 13 | no.ticket.cache { 14 | com.sun.security.auth.module.Krb5LoginModule 15 | required 16 | refreshKrb5Config=true 17 | storeKey=false 18 | useTicketCache=false 19 | debug=${debug} 20 | ; 21 | }; -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, out 2 | 3 | log4j.logger.org.elasticsearch.de.codecentric.elasticsearch=ALL 4 | log4j.logger.de.codecentric.elasticsearch=ALL 5 | #log4j.logger.net.sf.ehcache=ERROR 6 | #log4j.logger.org.apache.kerby=INFO 7 | #log4j.logger.org.apache.http=ALL 8 | 9 | #log4j.logger.org.apache.http=INFO, out 10 | #log4j.additivity.org.apache.http=false 11 | 12 | log4j.appender.out=org.apache.log4j.ConsoleAppender 13 | log4j.appender.out.layout=org.apache.log4j.PatternLayout 14 | log4j.appender.out.layout.conversionPattern=[%d{HH:mm:ss,SSS}][%-5p] %c{1} - %m%n 15 | -------------------------------------------------------------------------------- /src/test/java/org/elasticsearch/node/PluginEnabledNode.java: -------------------------------------------------------------------------------- 1 | package org.elasticsearch.node; 2 | 3 | import java.util.Collection; 4 | 5 | import org.elasticsearch.Version; 6 | import org.elasticsearch.common.settings.Settings; 7 | import org.elasticsearch.node.Node; 8 | import org.elasticsearch.node.internal.InternalSettingsPreparer; 9 | import org.elasticsearch.plugins.Plugin; 10 | 11 | public class PluginEnabledNode extends Node{ 12 | 13 | public PluginEnabledNode(Settings preparedSettings, Collection> classpathPlugins) { 14 | super(InternalSettingsPreparer.prepareEnvironment(preparedSettings, null), Version.CURRENT, classpathPlugins); 15 | } 16 | 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle/ 3 | *.iml 4 | *.ipr 5 | *.iws 6 | work/ 7 | /data/ 8 | logs/ 9 | .DS_Store 10 | build/ 11 | target/ 12 | *-execution-hints.log 13 | docs/html/ 14 | docs/build.log 15 | /tmp/ 16 | backwards/ 17 | html_docs 18 | .vagrant/ 19 | 20 | ## eclipse ignores (use 'mvn eclipse:eclipse' to build eclipse projects) 21 | ## All files (.project, .classpath, .settings/*) should be generated through Maven which 22 | ## will correctly set the classpath based on the declared dependencies and write settings 23 | ## files to ensure common coding style across Eclipse and IDEA. 24 | .project 25 | .classpath 26 | eclipse-build 27 | .settings 28 | 29 | ## netbeans ignores 30 | nb-configuration.xml 31 | nbactions.xml 32 | 33 | dependency-reduced-pom.xml 34 | integ_test_t* 35 | testtmp 36 | -------------------------------------------------------------------------------- /integration-tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /integ-test/start_elasticsearch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | cd $DIR/.. 4 | mvn -q -ff clean package || { echo 'build failed' ; exit -1; } 5 | TMP=target/integ_test_tmp 6 | rm -rf $TMP 7 | mkdir -p $TMP 8 | cd $TMP 9 | wget https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.0.0/elasticsearch-2.0.0.tar.gz 10 | tar -xzf elasticsearch-2.0.0.tar.gz 11 | cd elasticsearch-2.0.0 12 | bin/plugin install license 13 | bin/plugin install shield 14 | bin/shield/syskeygen 15 | bin/plugin remove elasticsearch-shield-kerberos-realm 16 | bin/plugin install file:///$DIR/../target/releases/elasticsearch-shield-kerberos-realm-2.0.0.zip 17 | echo "shield.authc.realms.kerb.type: cc-kerberos" > config/elasticsearch.yml 18 | echo "shield.authc.realms.kerb.order: 0" >> config/elasticsearch.yml 19 | echo "shield.authc.realms.kerb.acceptor_keytab_path: $DIR/../../kerby-dist/kdc-dist-1.0.0-RC1/http.keytab" >> config/elasticsearch.yml 20 | echo "shield.authc.realms.kerb.acceptor_principal: HTTP/localhost@EXAMPLE.COM" >> config/elasticsearch.yml 21 | echo "shield.authc.realms.kerb.roles: admin" >> config/elasticsearch.yml 22 | echo "de.codecentric.realm.cc-kerberos.krb5.file_path: $DIR/../../kerby-dist/kdc-dist-1.0.0-RC1/conf/krb5.conf" >> config/elasticsearch.yml 23 | echo "de.codecentric.realm.cc-kerberos.krb_debug: true" >> config/elasticsearch.yml 24 | echo "security.manager.enabled: false" >> config/elasticsearch.yml 25 | cat config/elasticsearch.yml 26 | bin/elasticsearch 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/support/SettingConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.support; 19 | 20 | import de.codecentric.elasticsearch.plugin.kerberosrealm.realm.KerberosRealm; 21 | 22 | public class SettingConstants { 23 | 24 | private static final String PREFIX = "de.codecentric.realm." + KerberosRealm.TYPE + "."; 25 | //public static final String JAAS_LOGIN_CONF_FILE_PATH = "jaas_login_conf.file_path"; 26 | public static final String STRIP_REALM_FROM_PRINCIPAL = "strip_realm_from_principal"; 27 | public static final String ACCEPTOR_KEYTAB_PATH = "acceptor_keytab_path"; 28 | public static final String ACCEPTOR_PRINCIPAL = "acceptor_principal"; 29 | public static final String ROLES = "roles"; 30 | 31 | public static final String KRB_DEBUG = PREFIX + "krb_debug"; 32 | public static final String KRB5_FILE_PATH = PREFIX + "krb5.file_path"; 33 | 34 | private SettingConstants() { 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/support/KrbConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.support; 19 | 20 | import org.ietf.jgss.GSSException; 21 | import org.ietf.jgss.Oid; 22 | 23 | public class KrbConstants { 24 | 25 | static { 26 | Oid spnegoTmp = null; 27 | try { 28 | spnegoTmp = new Oid("1.3.6.1.5.5.2"); 29 | } catch (final GSSException e) { 30 | 31 | } 32 | SPNEGO = spnegoTmp; 33 | } 34 | 35 | public static final Oid SPNEGO; 36 | public static final String KRB5_CONF_PROP = "java.security.krb5.conf"; 37 | public static final String JAAS_LOGIN_CONF_PROP = "java.security.auth.login.config"; 38 | public static final String USE_SUBJECT_CREDS_ONLY_PROP = "javax.security.auth.useSubjectCredsOnly"; 39 | public static final String NEGOTIATE = "Negotiate"; 40 | public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; 41 | 42 | private KrbConstants() { 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosRealmFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.realm; 19 | 20 | import org.elasticsearch.common.inject.Inject; 21 | import org.elasticsearch.shield.ShieldSettingsFilter; 22 | import org.elasticsearch.shield.authc.Realm; 23 | import org.elasticsearch.shield.authc.RealmConfig; 24 | 25 | /** 26 | */ 27 | public class KerberosRealmFactory extends Realm.Factory { 28 | 29 | private final ShieldSettingsFilter settingsFilter; 30 | 31 | @Inject 32 | public KerberosRealmFactory(final ShieldSettingsFilter settingsFilter) { 33 | super(KerberosRealm.TYPE, false); 34 | this.settingsFilter = settingsFilter; 35 | } 36 | 37 | @Override 38 | public KerberosRealm create(final RealmConfig config) { 39 | settingsFilter.filterOut("shield.authc.realms." + config.name() + ".*"); 40 | return new KerberosRealm(config); 41 | } 42 | 43 | @Override 44 | public KerberosRealm createDefault(final String name) { 45 | return null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.realm; 19 | 20 | import java.util.Objects; 21 | 22 | import org.elasticsearch.common.logging.ESLogger; 23 | import org.elasticsearch.common.logging.Loggers; 24 | import org.elasticsearch.shield.authc.AuthenticationToken; 25 | 26 | public class KerberosAuthenticationToken implements AuthenticationToken { 27 | 28 | static final KerberosAuthenticationToken LIVENESS_TOKEN = new KerberosAuthenticationToken(new byte[]{1,2,3}, "LIVENESS_TOKEN"); 29 | protected final ESLogger logger = Loggers.getLogger(this.getClass()); 30 | private byte[] outToken; 31 | private final String principal; 32 | 33 | public KerberosAuthenticationToken(final byte[] outToken, final String principal) { 34 | super(); 35 | this.outToken = Objects.requireNonNull(outToken); 36 | this.principal = Objects.requireNonNull(principal); 37 | } 38 | 39 | @Override 40 | public void clearCredentials() { 41 | this.outToken = null; 42 | logger.debug("credentials cleared for {}", toString()); 43 | } 44 | 45 | @Override 46 | public Object credentials() { 47 | return outToken; 48 | } 49 | 50 | @Override 51 | public String principal() { 52 | return principal; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return "KerberosAuthenticationToken [principal=" + principal + ", credentials null?: " + (outToken == null) + "]"; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/rest/LoginInfoRestAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.rest; 19 | 20 | import org.elasticsearch.client.Client; 21 | import org.elasticsearch.common.inject.Inject; 22 | import org.elasticsearch.common.settings.Settings; 23 | import org.elasticsearch.common.xcontent.XContentBuilder; 24 | import org.elasticsearch.rest.BaseRestHandler; 25 | import org.elasticsearch.rest.BytesRestResponse; 26 | import org.elasticsearch.rest.RestChannel; 27 | import org.elasticsearch.rest.RestController; 28 | import org.elasticsearch.rest.RestRequest; 29 | import org.elasticsearch.rest.RestRequest.Method; 30 | import org.elasticsearch.rest.RestStatus; 31 | import org.elasticsearch.shield.User; 32 | 33 | public class LoginInfoRestAction extends BaseRestHandler { 34 | 35 | @Inject 36 | public LoginInfoRestAction(final Settings settings, final RestController controller, final Client client) { 37 | super(settings, controller, client); 38 | controller.registerHandler(Method.GET, "/_logininfo", this); 39 | } 40 | 41 | @Override 42 | protected void handleRequest(final RestRequest request, final RestChannel channel, final Client client) throws Exception { 43 | BytesRestResponse response = null; 44 | final XContentBuilder builder = channel.newBuilder(); 45 | try { 46 | builder.startObject(); 47 | final User user = ((User) request.getFromContext("_shield_user")); 48 | if (user != null) { 49 | builder.field("principal", user.principal()); 50 | builder.field("roles", user.roles()); 51 | } else { 52 | builder.nullField("principal"); 53 | } 54 | 55 | builder.field("remote_address", request.getFromContext("_rest_remote_address")); 56 | builder.endObject(); 57 | response = new BytesRestResponse(RestStatus.OK, builder); 58 | } catch (final Exception e1) { 59 | builder.startObject(); 60 | builder.field("error", e1.toString()); 61 | builder.endObject(); 62 | response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); 63 | } 64 | 65 | channel.sendResponse(response); 66 | 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/KerberosRealmPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm; 19 | 20 | import java.nio.file.Paths; 21 | 22 | import org.elasticsearch.common.SuppressForbidden; 23 | import org.elasticsearch.common.logging.ESLogger; 24 | import org.elasticsearch.common.logging.Loggers; 25 | import org.elasticsearch.common.settings.Settings; 26 | import org.elasticsearch.plugins.Plugin; 27 | import org.elasticsearch.rest.RestModule; 28 | import org.elasticsearch.shield.authc.AuthenticationModule; 29 | 30 | import de.codecentric.elasticsearch.plugin.kerberosrealm.realm.KerberosAuthenticationFailureHandler; 31 | import de.codecentric.elasticsearch.plugin.kerberosrealm.realm.KerberosRealm; 32 | import de.codecentric.elasticsearch.plugin.kerberosrealm.realm.KerberosRealmFactory; 33 | import de.codecentric.elasticsearch.plugin.kerberosrealm.rest.LoginInfoRestAction; 34 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.PropertyUtil; 35 | 36 | /** 37 | */ 38 | public class KerberosRealmPlugin extends Plugin { 39 | 40 | protected final ESLogger logger = Loggers.getLogger(this.getClass()); 41 | private static final String CLIENT_TYPE = "client.type"; 42 | private final boolean client; 43 | private final Settings settings; 44 | 45 | public KerberosRealmPlugin(final Settings settings) { 46 | this.settings = settings; 47 | client = !"node".equals(settings.get(CLIENT_TYPE, "node")); 48 | logger.info("Start Kerberos Realm Plugin (mode: {})", settings.get(CLIENT_TYPE)); 49 | } 50 | 51 | @Override 52 | public String name() { 53 | return KerberosRealm.TYPE + "-realm"; 54 | } 55 | 56 | @Override 57 | public String description() { 58 | return "codecentric AG Kerberos V5 Realm"; 59 | } 60 | 61 | public void onModule(final RestModule module) { 62 | if (!client) { 63 | module.addRestAction(LoginInfoRestAction.class); 64 | } 65 | } 66 | 67 | @SuppressForbidden(reason = "proper use of Paths.get()") 68 | public void onModule(final AuthenticationModule authenticationModule) { 69 | if (!client) { 70 | PropertyUtil.initKerberosProps(settings, Paths.get("/")); 71 | authenticationModule.addCustomRealm(KerberosRealm.TYPE, KerberosRealmFactory.class); 72 | authenticationModule.setAuthenticationFailureHandler(KerberosAuthenticationFailureHandler.class); 73 | } else { 74 | logger.warn("This plugin is not necessary for client nodes"); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/de/codecentric/elasticsearch/plugin/kerberosrealm/support/EmbeddedKRBServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.support; 19 | 20 | import java.io.File; 21 | 22 | import org.apache.commons.io.FileUtils; 23 | import org.apache.kerby.kerberos.kdc.impl.NettyKdcServerImpl; 24 | import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; 25 | import org.apache.kerby.kerberos.kerb.spec.ticket.TgtTicket; 26 | import org.apache.kerby.util.NetworkUtil; 27 | import org.elasticsearch.common.SuppressForbidden; 28 | 29 | @SuppressForbidden(reason = "unit test") 30 | public class EmbeddedKRBServer { 31 | 32 | private SimpleKdcServer simpleKdcServer; 33 | private String realm = "CCK.COM"; 34 | 35 | public void start(final File workDir) throws Exception { 36 | simpleKdcServer = new SimpleKdcServer(); 37 | simpleKdcServer.enableDebug(); 38 | simpleKdcServer.setKdcTcpPort(NetworkUtil.getServerPort()); 39 | simpleKdcServer.setKdcUdpPort(NetworkUtil.getServerPort()); 40 | simpleKdcServer.setAllowTcp(true); 41 | simpleKdcServer.setAllowUdp(true); 42 | simpleKdcServer.setKdcRealm(realm); 43 | simpleKdcServer.setKdcHost("localhost"); 44 | FileUtils.forceMkdir(workDir); 45 | simpleKdcServer.setWorkDir(workDir); 46 | simpleKdcServer.setInnerKdcImpl(new NettyKdcServerImpl(simpleKdcServer.getKdcSetting())); 47 | simpleKdcServer.init(); 48 | //System.setErr(new PrintStream(new NullOutputStream())); 49 | simpleKdcServer.start(); 50 | } 51 | 52 | public SimpleKdcServer getSimpleKdcServer() { 53 | return simpleKdcServer; 54 | } 55 | 56 | public static void main(final String[] args) throws Exception { 57 | final File workDir = new File("."); 58 | final EmbeddedKRBServer eks = new EmbeddedKRBServer(); 59 | eks.realm = "DUMMY.COM"; 60 | eks.start(workDir); 61 | eks.getSimpleKdcServer().createPrincipal("kirk/admin@DUMMY.COM", "kirkpwd"); 62 | eks.getSimpleKdcServer().createPrincipal("uhura@DUMMY.COM", "uhurapwd"); 63 | eks.getSimpleKdcServer().createPrincipal("service/1@DUMMY.COM", "service1pwd"); 64 | eks.getSimpleKdcServer().createPrincipal("service/2@DUMMY.COM", "service2pwd"); 65 | eks.getSimpleKdcServer().exportPrincipal("service/1@DUMMY.COM", new File(workDir, "service1.keytab")); //server, acceptor 66 | eks.getSimpleKdcServer().exportPrincipal("service/2@DUMMY.COM", new File(workDir, "service2.keytab")); //server, acceptor 67 | 68 | eks.getSimpleKdcServer().createPrincipal("HTTP/localhost@DUMMY.COM", "httplocpwd"); 69 | eks.getSimpleKdcServer().exportPrincipal("HTTP/localhost@DUMMY.COM", new File(workDir, "httploc.keytab")); //server, acceptor 70 | 71 | eks.getSimpleKdcServer().createPrincipal("HTTP/localhost@DUMMY.COM", "httpcpwd"); 72 | eks.getSimpleKdcServer().exportPrincipal("HTTP/localhost@DUMMY.COM", 73 | new File(workDir, "http.keytab")); //server, acceptor 74 | 75 | final TgtTicket tgt = eks.getSimpleKdcServer().getKrbClient().requestTgtWithPassword("kirk/admin@DUMMY.COM", "kirkpwd"); 76 | eks.getSimpleKdcServer().getKrbClient().storeTicket(tgt, new File(workDir, "kirk.cc")); 77 | 78 | try { 79 | try { 80 | FileUtils.copyFile(new File("/etc/krb5.conf"), new File("/etc/krb5.conf.bak")); 81 | } catch (final Exception e) { 82 | //ignore 83 | } 84 | FileUtils.copyFileToDirectory(new File(workDir, "krb5.conf"), new File("/etc/")); 85 | System.out.println("Generated krb5.conf copied to /etc"); 86 | } catch (final Exception e) { 87 | System.out.println("Unable to copy generated krb5.conf to /etc diúe to " + e.getMessage()); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/support/PropertyUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.support; 19 | 20 | import java.io.FileNotFoundException; 21 | import java.nio.file.Files; 22 | import java.nio.file.Path; 23 | 24 | import org.elasticsearch.ExceptionsHelper; 25 | import org.elasticsearch.common.SuppressForbidden; 26 | import org.elasticsearch.common.logging.ESLogger; 27 | import org.elasticsearch.common.logging.Loggers; 28 | import org.elasticsearch.common.settings.Settings; 29 | import org.elasticsearch.env.Environment; 30 | 31 | public class PropertyUtil { 32 | 33 | private static final ESLogger log = Loggers.getLogger(PropertyUtil.class); 34 | 35 | private PropertyUtil() { 36 | } 37 | 38 | @SuppressForbidden(reason = "sysout needed here cause krb debug also goes to sysout") 39 | public static void initKerberosProps(final Settings settings, Path conf) { 40 | if (conf == null) { 41 | final Environment env = new Environment(settings); 42 | conf = env.configFile(); 43 | } 44 | PropertyUtil.setSystemProperty(KrbConstants.USE_SUBJECT_CREDS_ONLY_PROP, "false", false); 45 | //PropertyUtil.setSystemProperty(KrbConstants.USE_SUBJECT_CREDS_ONLY_PROP, "true", false); //TODO make strict 46 | try { 47 | PropertyUtil.setSystemPropertyToRelativeFile(KrbConstants.KRB5_CONF_PROP, conf, 48 | settings.get(SettingConstants.KRB5_FILE_PATH, "/etc/krb5.conf"), false, true); 49 | } catch (final FileNotFoundException e) { 50 | ExceptionsHelper.convertToElastic(e); 51 | } 52 | 53 | final boolean krbDebug = settings.getAsBoolean(SettingConstants.KRB_DEBUG, false); 54 | 55 | if (krbDebug) { 56 | System.out.println("Kerberos Realm debug is enabled"); 57 | log.error("NOT AN ERROR: Kerberos Realm debug is enabled"); 58 | JaasKrbUtil.ENABLE_DEBUG = true; 59 | System.setProperty("sun.security.krb5.debug", "true"); 60 | System.setProperty("java.security.debug", "all"); 61 | System.setProperty("java.security.auth.debug", "all"); 62 | System.setProperty("sun.security.spnego.debug", "true"); 63 | } else { 64 | log.info("Kerberos Realm debug is disabled"); 65 | } 66 | 67 | log.info(KrbConstants.KRB5_CONF_PROP + ": {}", System.getProperty(KrbConstants.KRB5_CONF_PROP)); 68 | 69 | } 70 | 71 | public static boolean setSystemPropertyToRelativeFile(final String property, final Path parentDir, final String relativeFileName, 72 | final boolean overwrite, final boolean checkFileExists) throws FileNotFoundException { 73 | if (relativeFileName == null) { 74 | log.error("Cannot set property " + property + " because filename is null"); 75 | return false; 76 | } 77 | final Path path = parentDir.resolve(relativeFileName).toAbsolutePath(); 78 | 79 | if (Files.isReadable(path) && !Files.isDirectory(path)) { 80 | return setSystemProperty(property, path.toString(), overwrite); 81 | } else { 82 | 83 | if (checkFileExists) { 84 | throw new FileNotFoundException(path.toString()); 85 | } 86 | 87 | log.error("Cannot read from {}, maybe the file does not exists? ", path.toString()); 88 | } 89 | return false; 90 | } 91 | 92 | public static boolean setSystemProperty(final String property, final String value, final boolean overwrite) { 93 | if (System.getProperty(property) == null) { 94 | if (value == null) { 95 | log.error("Cannot set property " + property + " because value is null"); 96 | return false; 97 | } 98 | log.info("Set system property {} to {}", property, value); 99 | System.setProperty(property, value); 100 | return true; 101 | } else { 102 | log.warn("Property " + property + " already set to " + System.getProperty(property)); 103 | if (overwrite) { 104 | log.info("Overwrite system property {} with {} (old value was {})", property, value, System.getProperty(property)); 105 | System.setProperty(property, value); 106 | return true; 107 | } 108 | } 109 | return false; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Shield Kerberos Realm 2 | ===================== 3 | 4 | [![Build Status](https://travis-ci.org/codecentric/elasticsearch-shield-kerberos-realm.svg?branch=master)](https://travis-ci.org/codecentric/elasticsearch-shield-kerberos-realm) 5 | [![Coverage Status](https://coveralls.io/repos/codecentric/elasticsearch-shield-kerberos-realm/badge.svg?branch=master&service=github)](https://coveralls.io/github/codecentric/elasticsearch-shield-kerberos-realm?branch=master) 6 | [![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) 7 | 8 | Kerberos/SPNEGO custom realm for Elasticsearch Shield 2.3.1. 9 | Authenticate HTTP and Transport requests via Kerberos/SPNEGO. 10 | 11 | ### License 12 | Apache License Version 2.0 13 | 14 | ### Features 15 | 16 | * Kerberos/SPNEGO REST/HTTP authentication 17 | * Kerberos/SPNEGO Transport authentication 18 | * No JAAS login.conf required 19 | * No external dependencies 20 | 21 | ### Community support 22 | [Stackoverflow](http://stackoverflow.com/questions/ask?tags=es-kerberos+elasticsearch) 23 | [Twitter @hendrikdev22](https://twitter.com/hendrikdev22) 24 | 25 | ### Commercial support 26 | Available. Please contact [vertrieb@codecentric.de](mailto:vertrieb@codecentric.de) 27 | 28 | ### Prerequisites 29 | 30 | * Elasticsearch 2.3.1 31 | * Shield Plugin 2.3.1 32 | * Kerberos Infrastructure (ActiveDirectory, MIT, Heimdal, ...) 33 | 34 | ### Install release 35 | [Download latest release](https://github.com/codecentric/elasticsearch-shield-kerberos-realm/releases) and store it somewhere. Then execute: 36 | 37 | $ bin/plugin install file:///path/to/target/release/elasticsearch-shield-kerberos-realm-2.3.1.zip 38 | 39 | ### Build and install latest 40 | $ git clone https://github.com/codecentric/elasticsearch-shield-kerberos-realm.git 41 | $ mvn package 42 | $ bin/plugin install file:///path/to/target/release/elasticsearch-shield-kerberos-realm-2.3.1.zip 43 | 44 | ### Configuration 45 | 46 | Configuration is done in elasticsearch.yml 47 | 48 | shield.authc.realms.cc-kerberos.type: cc-kerberos 49 | shield.authc.realms.cc-kerberos.order: 0 50 | shield.authc.realms.cc-kerberos.acceptor_keytab_path: /path/to/server.keytab 51 | shield.authc.realms.cc-kerberos.acceptor_principal: HTTP/localhost@REALM.COM 52 | shield.authc.realms.cc-kerberos.roles: role1, role2 53 | shield.authc.realms.cc-kerberos.strip_realm_from_principal: true 54 | de.codecentric.realm.cc-kerberos.krb5.file_path: /etc/krb5.conf 55 | de.codecentric.realm.cc-kerberos.krb_debug: false 56 | security.manager.enabled: false 57 | 58 | * ``acceptor_keytab_path`` - The absolute path to the keytab where the acceptor_principal credentials are stored. 59 | * ``acceptor_principal`` - Acceptor (Server) Principal name, must be present in acceptor_keytab_path file 60 | * ``roles`` - Roles which should be assigned to the initiator (the user who's logged in) 61 | * ``strip_realm_from_principal`` - If true then the realm will be stripped from the user name 62 | * ``de.codecentric.realm.cc-kerberos.krb_debug`` - If true a whole bunch of kerberos/security related debugging output will be logged to standard out 63 | * ``de.codecentric.realm.cc-kerberos.krb5.file_path`` - Absolute path to krb5.conf file. 64 | * ``security.manager.enabled`` - Must currently be set to ``false``. This will likely change with Elasticsearch 2.2, see [PR 14108](https://github.com/elastic/elasticsearch/pull/14108) 65 | 66 | 67 | ### REST/HTTP authentication 68 | 69 | $ kinit 70 | $ curl --negotiate -u : "http://localhost:9200/_logininfo?pretty" 71 | 72 | Or with a browser that supports SPNEGO like Chrome or Firefox 73 | 74 | ### Transport authentication 75 | 76 | try (TransportClient client = TransportClient.builder().settings(settings).build()) { 77 | client.addTransportAddress(nodes[0].getTransport().address().publishAddress()); 78 | try (KerberizedClient kc = new KerberizedClient(client, 79 | "user@REALM.COM", 80 | "secret", 81 | "HTTP/localhost@REALM.COM")) { 82 | 83 | ClusterHealthResponse response = kc.admin().cluster().prepareHealth().execute().actionGet(); 84 | assertThat(response.isTimedOut(), is(false)); 85 | } 86 | } 87 | 88 | #### Login with password 89 | KerberizedClient kc = new KerberizedClient(client, 90 | "user@REALM.COM", 91 | "secret", 92 | "HTTP/localhost@REALM.COM") 93 | 94 | #### Login with (client side) keytab 95 | KerberizedClient kc = new KerberizedClient(client, 96 | Paths.get("client.keytab"), 97 | "user@REALM.COM", 98 | "HTTP/localhost@REALM.COM") 99 | 100 | #### Login with TGT (Ticket) 101 | KerberizedClient kc = new KerberizedClient(client, 102 | "user@REALM.COM", 103 | Paths.get("ticket.cc"), 104 | "HTTP/localhost@REALM.COM") 105 | 106 | #### Login with javax.security.auth.Subject 107 | KerberizedClient kc = new KerberizedClient(client, 108 | subject, 109 | "HTTP/localhost@REALM.COM") 110 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosAuthenticationFailureHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.realm; 19 | 20 | import org.elasticsearch.ElasticsearchException; 21 | import org.elasticsearch.ElasticsearchSecurityException; 22 | import org.elasticsearch.common.logging.ESLogger; 23 | import org.elasticsearch.common.logging.Loggers; 24 | import org.elasticsearch.rest.RestRequest; 25 | import org.elasticsearch.shield.authc.AuthenticationToken; 26 | import org.elasticsearch.shield.authc.DefaultAuthenticationFailureHandler; 27 | import org.elasticsearch.transport.TransportMessage; 28 | 29 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.KrbConstants; 30 | 31 | /** 32 | */ 33 | public class KerberosAuthenticationFailureHandler extends DefaultAuthenticationFailureHandler { 34 | 35 | protected final ESLogger logger = Loggers.getLogger(this.getClass()); 36 | 37 | @Override 38 | public ElasticsearchSecurityException unsuccessfulAuthentication(final RestRequest request, final AuthenticationToken token) { 39 | final ElasticsearchSecurityException e = super.unsuccessfulAuthentication(request, token); 40 | e.addHeader(KrbConstants.WWW_AUTHENTICATE, KrbConstants.NEGOTIATE); 41 | if (logger.isDebugEnabled()) { 42 | logger.debug("unsuccessfulAuthentication for rest request and token {}", token); 43 | } 44 | return e; 45 | } 46 | 47 | @Override 48 | public ElasticsearchSecurityException missingToken(final RestRequest request) { 49 | final ElasticsearchSecurityException e = super.missingToken(request); 50 | e.addHeader(KrbConstants.WWW_AUTHENTICATE, KrbConstants.NEGOTIATE); 51 | if (logger.isDebugEnabled()) { 52 | logger.debug("missing token for rest request"); 53 | } 54 | return e; 55 | } 56 | 57 | @Override 58 | public ElasticsearchSecurityException exceptionProcessingRequest(final RestRequest request, final Exception e) { 59 | final ElasticsearchSecurityException se = super.exceptionProcessingRequest(request, e); 60 | String outToken = ""; 61 | if (e instanceof ElasticsearchException) { 62 | final ElasticsearchException kae = (ElasticsearchException) e; 63 | if (kae.getHeader("kerberos_out_token") != null) { 64 | outToken = " " + kae.getHeader("kerberos_out_token").get(0); 65 | } 66 | } 67 | 68 | se.addHeader(KrbConstants.WWW_AUTHENTICATE, KrbConstants.NEGOTIATE + outToken); 69 | 70 | if (logger.isDebugEnabled()) { 71 | logger.debug("exception for rest request: {}", e.toString()); 72 | } 73 | 74 | return se; 75 | } 76 | 77 | @Override 78 | public ElasticsearchSecurityException authenticationRequired(final String action) { 79 | final ElasticsearchSecurityException se = super.authenticationRequired(action); 80 | se.addHeader(KrbConstants.WWW_AUTHENTICATE, KrbConstants.NEGOTIATE); 81 | 82 | if (logger.isDebugEnabled()) { 83 | logger.debug("authentication required for action {}", action); 84 | } 85 | return se; 86 | } 87 | 88 | @Override 89 | public ElasticsearchSecurityException exceptionProcessingRequest(final TransportMessage message, final Exception e) { 90 | final ElasticsearchSecurityException se = super.exceptionProcessingRequest(message, e); 91 | String outToken = ""; 92 | 93 | if (e instanceof ElasticsearchException) { 94 | final ElasticsearchException kae = (ElasticsearchException) e; 95 | if (kae.getHeader("kerberos_out_token") != null) { 96 | outToken = " " + kae.getHeader("kerberos_out_token").get(0); 97 | } 98 | } 99 | se.addHeader(KrbConstants.WWW_AUTHENTICATE, KrbConstants.NEGOTIATE + outToken); 100 | 101 | if (logger.isDebugEnabled()) { 102 | logger.debug("exception for transport message: {}", e.toString()); 103 | } 104 | 105 | return se; 106 | } 107 | 108 | @Override 109 | public ElasticsearchSecurityException missingToken(final TransportMessage message, final String action) { 110 | final ElasticsearchSecurityException se = super.missingToken(message, action); 111 | se.addHeader(KrbConstants.WWW_AUTHENTICATE, KrbConstants.NEGOTIATE); 112 | 113 | if (logger.isDebugEnabled()) { 114 | logger.debug("missing token for {} transport message", action); 115 | } 116 | 117 | return se; 118 | } 119 | 120 | @Override 121 | public ElasticsearchSecurityException unsuccessfulAuthentication(final TransportMessage message, final AuthenticationToken token, 122 | final String action) { 123 | final ElasticsearchSecurityException se = super.unsuccessfulAuthentication(message, token, action); 124 | se.addHeader(KrbConstants.WWW_AUTHENTICATE, KrbConstants.NEGOTIATE); 125 | 126 | if (logger.isDebugEnabled()) { 127 | logger.debug("unsuccessfulAuthentication for {} transport message and token {}", action, token); 128 | } 129 | 130 | return se; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/support/JaasKrbUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Kerby Project, Apache Software Foundation, https://directory.apache.org/kerby/ 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.support; 19 | 20 | //taken from the apache kerby project 21 | //https://directory.apache.org/kerby/ 22 | 23 | import java.io.IOException; 24 | import java.nio.file.Path; 25 | import java.security.Principal; 26 | import java.util.HashMap; 27 | import java.util.HashSet; 28 | import java.util.Map; 29 | import java.util.Set; 30 | 31 | import javax.security.auth.Subject; 32 | import javax.security.auth.callback.Callback; 33 | import javax.security.auth.callback.CallbackHandler; 34 | import javax.security.auth.callback.PasswordCallback; 35 | import javax.security.auth.callback.UnsupportedCallbackException; 36 | import javax.security.auth.kerberos.KerberosPrincipal; 37 | import javax.security.auth.login.AppConfigurationEntry; 38 | import javax.security.auth.login.Configuration; 39 | import javax.security.auth.login.LoginContext; 40 | import javax.security.auth.login.LoginException; 41 | 42 | /** 43 | * JAAS utilities for Kerberos login. 44 | */ 45 | public final class JaasKrbUtil { 46 | 47 | public static boolean ENABLE_DEBUG = false; 48 | 49 | private JaasKrbUtil() { 50 | } 51 | 52 | public static Subject loginUsingPassword(final String principal, final String password) throws LoginException { 53 | final Set principals = new HashSet(); 54 | principals.add(new KerberosPrincipal(principal)); 55 | 56 | final Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); 57 | 58 | final Configuration conf = usePassword(principal); 59 | final String confName = "PasswordConf"; 60 | final CallbackHandler callback = new KrbCallbackHandler(principal, password); 61 | final LoginContext loginContext = new LoginContext(confName, subject, callback, conf); 62 | loginContext.login(); 63 | return loginContext.getSubject(); 64 | } 65 | 66 | public static Subject loginUsingTicketCache(final String principal, final Path cachePath) throws LoginException { 67 | final Set principals = new HashSet(); 68 | principals.add(new KerberosPrincipal(principal)); 69 | 70 | final Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); 71 | 72 | final Configuration conf = useTicketCache(principal, cachePath); 73 | final String confName = "TicketCacheConf"; 74 | final LoginContext loginContext = new LoginContext(confName, subject, null, conf); 75 | loginContext.login(); 76 | return loginContext.getSubject(); 77 | } 78 | 79 | public static Subject loginUsingKeytab(final String principal, final Path keytabPath, final boolean initiator) throws LoginException { 80 | final Set principals = new HashSet(); 81 | principals.add(new KerberosPrincipal(principal)); 82 | 83 | final Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); 84 | 85 | final Configuration conf = useKeytab(principal, keytabPath, initiator); 86 | final String confName = "KeytabConf"; 87 | final LoginContext loginContext = new LoginContext(confName, subject, null, conf); 88 | loginContext.login(); 89 | return loginContext.getSubject(); 90 | } 91 | 92 | public static Configuration usePassword(final String principal) { 93 | return new PasswordJaasConf(principal); 94 | } 95 | 96 | public static Configuration useTicketCache(final String principal, final Path credentialPath) { 97 | return new TicketCacheJaasConf(principal, credentialPath); 98 | } 99 | 100 | public static Configuration useKeytab(final String principal, final Path keytabPath, final boolean initiator) { 101 | return new KeytabJaasConf(principal, keytabPath, initiator); 102 | } 103 | 104 | private static String getKrb5LoginModuleName() { 105 | return System.getProperty("java.vendor").contains("IBM") ? "com.ibm.security.auth.module.Krb5LoginModule" 106 | : "com.sun.security.auth.module.Krb5LoginModule"; 107 | } 108 | 109 | static class KeytabJaasConf extends Configuration { 110 | private final String principal; 111 | private final Path keytabPath; 112 | private final boolean initiator; 113 | 114 | public KeytabJaasConf(final String principal, final Path keytab, final boolean initiator) { 115 | this.principal = principal; 116 | this.keytabPath = keytab; 117 | this.initiator = initiator; 118 | } 119 | 120 | @Override 121 | public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { 122 | final Map options = new HashMap(); 123 | options.put("keyTab", keytabPath.toAbsolutePath().toString()); 124 | options.put("principal", principal); 125 | options.put("useKeyTab", "true"); 126 | options.put("storeKey", "true"); 127 | options.put("doNotPrompt", "true"); 128 | options.put("renewTGT", "false"); 129 | options.put("refreshKrb5Config", "true"); 130 | options.put("isInitiator", String.valueOf(initiator)); 131 | options.put("debug", String.valueOf(ENABLE_DEBUG)); 132 | 133 | return new AppConfigurationEntry[] { new AppConfigurationEntry(getKrb5LoginModuleName(), 134 | AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; 135 | } 136 | } 137 | 138 | static class TicketCacheJaasConf extends Configuration { 139 | private final String principal; 140 | private final Path clientCredentialPath; 141 | 142 | public TicketCacheJaasConf(final String principal, final Path clientCredentialPath) { 143 | this.principal = principal; 144 | this.clientCredentialPath = clientCredentialPath; 145 | } 146 | 147 | @Override 148 | public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { 149 | final Map options = new HashMap(); 150 | options.put("principal", principal); 151 | options.put("storeKey", "false"); 152 | options.put("doNotPrompt", "false"); 153 | options.put("useTicketCache", "true"); 154 | options.put("renewTGT", "true"); 155 | options.put("refreshKrb5Config", "true"); 156 | options.put("isInitiator", "true"); 157 | options.put("ticketCache", clientCredentialPath.toAbsolutePath().toString()); 158 | options.put("debug", String.valueOf(ENABLE_DEBUG)); 159 | 160 | return new AppConfigurationEntry[] { new AppConfigurationEntry(getKrb5LoginModuleName(), 161 | AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; 162 | } 163 | } 164 | 165 | static class PasswordJaasConf extends Configuration { 166 | private final String principal; 167 | 168 | public PasswordJaasConf(final String principal) { 169 | this.principal = principal; 170 | } 171 | 172 | @Override 173 | public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { 174 | final Map options = new HashMap<>(); 175 | options.put("principal", principal); 176 | options.put("storeKey", "true"); 177 | options.put("useTicketCache", "true"); 178 | options.put("useKeyTab", "false"); 179 | options.put("renewTGT", "true"); 180 | options.put("refreshKrb5Config", "true"); 181 | options.put("isInitiator", "true"); 182 | options.put("debug", String.valueOf(ENABLE_DEBUG)); 183 | 184 | return new AppConfigurationEntry[] { new AppConfigurationEntry(getKrb5LoginModuleName(), 185 | AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; 186 | } 187 | } 188 | 189 | public static class KrbCallbackHandler implements CallbackHandler { 190 | private final String principal; 191 | private final String password; 192 | 193 | public KrbCallbackHandler(final String principal, final String password) { 194 | this.principal = principal; 195 | this.password = password; 196 | } 197 | 198 | @Override 199 | public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException { 200 | for (int i = 0; i < callbacks.length; i++) { 201 | if (callbacks[i] instanceof PasswordCallback) { 202 | final PasswordCallback pc = (PasswordCallback) callbacks[i]; 203 | if (pc.getPrompt().contains(principal)) { 204 | pc.setPassword(password.toCharArray()); 205 | break; 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | org.elasticsearch.plugin 6 | plugins 7 | 2.3.1 8 | 9 | 4.0.0 10 | 11 | elasticsearch-shield-kerberos-realm 12 | Shield Kerberos V5 Realm 13 | Shield Kerberos V5 Realm by codecentric AG 14 | 15 | 16 | 17 | de.codecentric.elasticsearch.plugin.kerberosrealm.KerberosRealmPlugin 18 | 20 | false 21 | 22 | 24 | ${project.basedir}/integration-tests.xml 25 | license,shield,elasticsearch-shield-kerberos-realm 26 | -Xlint:-rawtypes 27 | true 28 | 29 | 30 | 31 | 32 | oss-snapshots 33 | Sonatype OSS Snapshots 34 | https://oss.sonatype.org/content/repositories/snapshots/ 35 | 36 | false 37 | 38 | 39 | true 40 | always 41 | 42 | 43 | 44 | elasticsearch-releases 45 | http://maven.elasticsearch.org/releases 46 | 47 | true 48 | daily 49 | 50 | 51 | false 52 | 53 | 54 | 55 | jspresso.org 56 | http://repository.jspresso.org/maven2/ 57 | 58 | 59 | 60 | 61 | 62 | org.elasticsearch 63 | elasticsearch 64 | ${project.version} 65 | provided 66 | 67 | 68 | org.elasticsearch.plugin 69 | shield 70 | ${project.version} 71 | provided 72 | 73 | 74 | org.elasticsearch.plugin 75 | license 76 | ${elasticsearch.version} 77 | provided 78 | 79 | 80 | 81 | 82 | 83 | commons-io 84 | commons-io 85 | 2.4 86 | test 87 | 88 | 89 | org.apache.httpcomponents 90 | fluent-hc 91 | 4.5.1 92 | test 93 | 94 | 95 | org.apache.httpcomponents 96 | httpclient 97 | 4.5.1 98 | test 99 | 100 | 101 | 102 | org.apache.kerby 103 | kerb-simplekdc 104 | 1.0.0-RC1 105 | test 106 | 107 | 108 | org.apache.kerby 109 | kerby-kdc 110 | 1.0.0-RC1 111 | test 112 | 113 | 114 | net.sourceforge.spnego 115 | spnego 116 | 7.0 117 | test 118 | 119 | 120 | org.slf4j 121 | slf4j-log4j12 122 | 1.7.12 123 | test 124 | 125 | 126 | 127 | 128 | 129 | org.apache.maven.plugins 130 | maven-jar-plugin 131 | 2.6 132 | 133 | 134 | 135 | test-jar 136 | 137 | 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-assembly-plugin 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-dependency-plugin 147 | 148 | 149 | integ-setup-dependencies 150 | pre-integration-test 151 | 152 | copy 153 | 154 | 155 | ${skip.integ.tests} 156 | true 157 | ${integ.deps}/plugins 158 | 159 | 160 | 161 | 162 | org.elasticsearch.distribution.zip 163 | elasticsearch 164 | ${elasticsearch.version} 165 | zip 166 | true 167 | ${integ.deps} 168 | 169 | 170 | 171 | 172 | org.elasticsearch.plugin 173 | license 174 | ${elasticsearch.version} 175 | zip 176 | true 177 | 178 | 179 | 180 | org.elasticsearch.plugin 181 | shield 182 | ${elasticsearch.version} 183 | zip 184 | true 185 | 186 | 187 | 188 | 189 | ${project.groupId} 190 | ${project.artifactId} 191 | ${project.version} 192 | zip 193 | true 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | org.apache.maven.plugins 202 | maven-antrun-plugin 203 | 204 | 205 | check-license 206 | none 207 | 208 | 209 | 210 | 211 | ant-contrib 212 | ant-contrib 213 | 1.0b3 214 | 215 | 216 | ant 217 | ant 218 | 219 | 220 | 221 | 222 | org.apache.ant 223 | ant-nodeps 224 | 1.8.1 225 | 226 | 227 | 228 | 229 | com.mycila 230 | license-maven-plugin 231 | 232 |
com/mycila/maven/plugin/license/templates/APACHE-2.txt
233 | 234 | 235 | 236 | **/* 237 | 238 |
239 | 240 | 241 | 242 | check 243 | 244 | 245 | 246 |
247 | 248 | org.eluder.coveralls 249 | coveralls-maven-plugin 250 | 4.1.0 251 | 252 |
253 |
254 |
255 | -------------------------------------------------------------------------------- /src/test/java/de/codecentric/elasticsearch/plugin/kerberosrealm/client/MockingKerberizedClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.client; 19 | 20 | import java.io.InputStream; 21 | import java.io.OutputStream; 22 | import java.nio.charset.StandardCharsets; 23 | 24 | import javax.security.auth.Subject; 25 | import javax.xml.bind.DatatypeConverter; 26 | 27 | import org.elasticsearch.action.ActionRequest; 28 | import org.elasticsearch.client.Client; 29 | import org.elasticsearch.common.SuppressForbidden; 30 | import org.ietf.jgss.ChannelBinding; 31 | import org.ietf.jgss.GSSContext; 32 | import org.ietf.jgss.GSSCredential; 33 | import org.ietf.jgss.GSSException; 34 | import org.ietf.jgss.GSSName; 35 | import org.ietf.jgss.MessageProp; 36 | import org.ietf.jgss.Oid; 37 | 38 | @SuppressForbidden(reason = "unit test") 39 | public class MockingKerberizedClient extends KerberizedClient { 40 | 41 | public MockingKerberizedClient(final Client in) { 42 | super(in, new Subject(), "mock_principal"); 43 | } 44 | 45 | @Override 46 | GSSContext initGSS() throws Exception { 47 | return new MockGSSContext(); 48 | } 49 | 50 | @Override 51 | void addAdditionalHeader(final ActionRequest request, final int count, final byte[] data) { 52 | if (count >= 4) { 53 | request.putHeader("Authorization", "Negotiate_c " + DatatypeConverter.printBase64Binary(data)); 54 | } 55 | } 56 | 57 | @SuppressForbidden(reason = "unit test") 58 | private static class MockGSSContext implements GSSContext { 59 | 60 | @Override 61 | public byte[] initSecContext(final byte[] inputBuf, final int offset, final int len) throws GSSException { 62 | if (inputBuf == null || inputBuf.length == 0) { 63 | return "mocked_initial_gss_security_context".getBytes(StandardCharsets.UTF_8); 64 | } else { 65 | return ("|" + new String(inputBuf, offset, len, StandardCharsets.UTF_8)).getBytes(StandardCharsets.UTF_8); 66 | } 67 | } 68 | 69 | @Override 70 | public int initSecContext(final InputStream inStream, final OutputStream outStream) throws GSSException { 71 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 72 | } 73 | 74 | @Override 75 | public byte[] acceptSecContext(final byte[] inToken, final int offset, final int len) throws GSSException { 76 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 77 | } 78 | 79 | @Override 80 | public void acceptSecContext(final InputStream inStream, final OutputStream outStream) throws GSSException { 81 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 82 | } 83 | 84 | @Override 85 | public boolean isEstablished() { 86 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 87 | } 88 | 89 | @Override 90 | public void dispose() throws GSSException { 91 | } 92 | 93 | @Override 94 | public int getWrapSizeLimit(final int qop, final boolean confReq, final int maxTokenSize) throws GSSException { 95 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 96 | } 97 | 98 | @Override 99 | public byte[] wrap(final byte[] inBuf, final int offset, final int len, final MessageProp msgProp) throws GSSException { 100 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 101 | } 102 | 103 | @Override 104 | public void wrap(final InputStream inStream, final OutputStream outStream, final MessageProp msgProp) throws GSSException { 105 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 106 | 107 | } 108 | 109 | @Override 110 | public byte[] unwrap(final byte[] inBuf, final int offset, final int len, final MessageProp msgProp) throws GSSException { 111 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 112 | } 113 | 114 | @Override 115 | public void unwrap(final InputStream inStream, final OutputStream outStream, final MessageProp msgProp) throws GSSException { 116 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 117 | } 118 | 119 | @Override 120 | public byte[] getMIC(final byte[] inMsg, final int offset, final int len, final MessageProp msgProp) throws GSSException { 121 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 122 | } 123 | 124 | @Override 125 | public void getMIC(final InputStream inStream, final OutputStream outStream, final MessageProp msgProp) throws GSSException { 126 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 127 | } 128 | 129 | @Override 130 | public void verifyMIC(final byte[] inToken, final int tokOffset, final int tokLen, final byte[] inMsg, final int msgOffset, 131 | final int msgLen, final MessageProp msgProp) throws GSSException { 132 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 133 | } 134 | 135 | @Override 136 | public void verifyMIC(final InputStream tokStream, final InputStream msgStream, final MessageProp msgProp) throws GSSException { 137 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 138 | } 139 | 140 | @Override 141 | public byte[] export() throws GSSException { 142 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 143 | } 144 | 145 | @Override 146 | public void requestMutualAuth(final boolean state) throws GSSException { 147 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 148 | } 149 | 150 | @Override 151 | public void requestReplayDet(final boolean state) throws GSSException { 152 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 153 | } 154 | 155 | @Override 156 | public void requestSequenceDet(final boolean state) throws GSSException { 157 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 158 | } 159 | 160 | @Override 161 | public void requestCredDeleg(final boolean state) throws GSSException { 162 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 163 | } 164 | 165 | @Override 166 | public void requestAnonymity(final boolean state) throws GSSException { 167 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 168 | } 169 | 170 | @Override 171 | public void requestConf(final boolean state) throws GSSException { 172 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 173 | } 174 | 175 | @Override 176 | public void requestInteg(final boolean state) throws GSSException { 177 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 178 | } 179 | 180 | @Override 181 | public void requestLifetime(final int lifetime) throws GSSException { 182 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 183 | } 184 | 185 | @Override 186 | public void setChannelBinding(final ChannelBinding cb) throws GSSException { 187 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 188 | } 189 | 190 | @Override 191 | public boolean getCredDelegState() { 192 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 193 | } 194 | 195 | @Override 196 | public boolean getMutualAuthState() { 197 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 198 | } 199 | 200 | @Override 201 | public boolean getReplayDetState() { 202 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 203 | } 204 | 205 | @Override 206 | public boolean getSequenceDetState() { 207 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 208 | } 209 | 210 | @Override 211 | public boolean getAnonymityState() { 212 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 213 | } 214 | 215 | @Override 216 | public boolean isTransferable() throws GSSException { 217 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 218 | } 219 | 220 | @Override 221 | public boolean isProtReady() { 222 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 223 | } 224 | 225 | @Override 226 | public boolean getConfState() { 227 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 228 | } 229 | 230 | @Override 231 | public boolean getIntegState() { 232 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 233 | } 234 | 235 | @Override 236 | public int getLifetime() { 237 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 238 | } 239 | 240 | @Override 241 | public GSSName getSrcName() throws GSSException { 242 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 243 | } 244 | 245 | @Override 246 | public GSSName getTargName() throws GSSException { 247 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 248 | } 249 | 250 | @Override 251 | public Oid getMech() throws GSSException { 252 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 253 | } 254 | 255 | @Override 256 | public GSSCredential getDelegCred() throws GSSException { 257 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 258 | } 259 | 260 | @Override 261 | public boolean isInitiator() throws GSSException { 262 | throw new UnsupportedOperationException("mock gss context does not support this operation"); 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/client/KerberizedClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm.client; 19 | 20 | import java.nio.file.Path; 21 | import java.nio.file.Paths; 22 | import java.security.PrivilegedExceptionAction; 23 | import java.util.List; 24 | import java.util.Locale; 25 | import java.util.Objects; 26 | 27 | import javax.security.auth.Subject; 28 | import javax.security.auth.login.LoginException; 29 | import javax.xml.bind.DatatypeConverter; 30 | 31 | import org.elasticsearch.ElasticsearchException; 32 | import org.elasticsearch.ElasticsearchSecurityException; 33 | import org.elasticsearch.ExceptionsHelper; 34 | import org.elasticsearch.action.Action; 35 | import org.elasticsearch.action.ActionListener; 36 | import org.elasticsearch.action.ActionRequest; 37 | import org.elasticsearch.action.ActionRequestBuilder; 38 | import org.elasticsearch.action.ActionResponse; 39 | import org.elasticsearch.client.Client; 40 | import org.elasticsearch.client.FilterClient; 41 | import org.elasticsearch.common.SuppressForbidden; 42 | import org.elasticsearch.common.logging.ESLogger; 43 | import org.elasticsearch.common.logging.Loggers; 44 | import org.ietf.jgss.GSSContext; 45 | import org.ietf.jgss.GSSCredential; 46 | import org.ietf.jgss.GSSException; 47 | import org.ietf.jgss.GSSManager; 48 | import org.ietf.jgss.GSSName; 49 | 50 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.JaasKrbUtil; 51 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.KrbConstants; 52 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.PropertyUtil; 53 | 54 | /** 55 | * 56 | * @author salyh 57 | * 58 | */ 59 | public class KerberizedClient extends FilterClient { 60 | 61 | protected final ESLogger logger = Loggers.getLogger(this.getClass()); 62 | private final Subject initiatorSubject; 63 | private final String acceptorPrincipal; 64 | 65 | /** 66 | * 67 | * @param in 68 | * @param initiatorSubject 69 | * @param acceptorPrincipal 70 | */ 71 | @SuppressForbidden(reason = "only used external") 72 | public KerberizedClient(final Client in, final Subject initiatorSubject, final String acceptorPrincipal) { 73 | super(in); 74 | PropertyUtil.initKerberosProps(settings, Paths.get("/")); 75 | this.initiatorSubject = Objects.requireNonNull(initiatorSubject); 76 | this.acceptorPrincipal = Objects.requireNonNull(acceptorPrincipal); 77 | } 78 | 79 | /** 80 | * 81 | * @param in 82 | * @param initiatorPrincipal 83 | * @param tgtTicketCache 84 | * make sure youre allowed to read from here es sec man 85 | * @param acceptorPrincipal 86 | * @throws LoginException 87 | */ 88 | public KerberizedClient(final Client in, final String initiatorPrincipal, final Path tgtTicketCache, final String acceptorPrincipal) 89 | throws LoginException { 90 | this(in, JaasKrbUtil.loginUsingTicketCache(initiatorPrincipal, tgtTicketCache), acceptorPrincipal); 91 | } 92 | 93 | /** 94 | * 95 | * @param in 96 | * @param initiatorPrincipal 97 | * @param initiatorPrincipalPassword 98 | * @param acceptorPrincipal 99 | * @throws LoginException 100 | */ 101 | public KerberizedClient(final Client in, final String initiatorPrincipal, final String initiatorPrincipalPassword, 102 | final String acceptorPrincipal) throws LoginException { 103 | this(in, JaasKrbUtil.loginUsingPassword(initiatorPrincipal, initiatorPrincipalPassword), acceptorPrincipal); 104 | } 105 | 106 | /** 107 | * 108 | * @param in 109 | * @param keyTabFile 110 | * @param initiatorPrincipal 111 | * @param acceptorPrincipal 112 | * @throws LoginException 113 | */ 114 | public KerberizedClient(final Client in, final Path keyTabFile, final String initiatorPrincipal, final String acceptorPrincipal) 115 | throws LoginException { 116 | this(in, JaasKrbUtil.loginUsingKeytab(initiatorPrincipal, keyTabFile, true), acceptorPrincipal); 117 | } 118 | 119 | @Override 120 | protected final > void doExecute( 121 | final Action action, final Request request, final ActionListener listener) { 122 | 123 | GSSContext context; 124 | try { 125 | context = initGSS(); 126 | //TODO subject logout 127 | } catch (final Exception e) { 128 | logger.error("Error creating gss context {}", e, e.toString()); 129 | listener.onFailure(e); 130 | return; 131 | } 132 | 133 | if (request.getHeader("Authorization") == null) { 134 | 135 | byte[] data; 136 | try { 137 | data = context.initSecContext(new byte[0], 0, 0); 138 | //TODO subject logout 139 | } catch (final Exception e) { 140 | logger.error("Error creating gss context {}", e, e.toString()); 141 | listener.onFailure(e); 142 | return; 143 | } 144 | 145 | request.putHeader("Authorization", "Negotiate " + DatatypeConverter.printBase64Binary(data)); 146 | logger.debug("Initial gss context round"); 147 | } else { 148 | logger.debug("Non-Initial gss context round: {}", request.getHeader("Authorization")); 149 | } 150 | 151 | final ActionListener newListener = (ActionListener) ((listener instanceof KerberosActionListener) ? listener 152 | : new KerberosActionListener(listener, action, request, context)); 153 | 154 | super.doExecute(action, request, newListener); 155 | } 156 | 157 | private class KerberosActionListener implements ActionListener { 158 | private final ActionListener inner; 159 | private final Action action; 160 | private final ActionRequest request; 161 | private final GSSContext context; 162 | private volatile int count; 163 | 164 | private KerberosActionListener(final ActionListener inner, final Action action, final ActionRequest request, 165 | final GSSContext context) { 166 | super(); 167 | this.inner = inner; 168 | this.action = action; 169 | this.request = request; 170 | this.context = context; 171 | } 172 | 173 | @Override 174 | public void onResponse(final ActionResponse response) { 175 | inner.onResponse(response); 176 | } 177 | 178 | @Override 179 | public void onFailure(final Throwable e) { 180 | 181 | final Throwable cause = ExceptionsHelper.unwrapCause(e); 182 | 183 | if (cause instanceof ElasticsearchSecurityException) { 184 | final ElasticsearchSecurityException securityException = (ElasticsearchSecurityException) cause; 185 | 186 | if (++count > 100) { 187 | inner.onFailure(new ElasticsearchException("kerberos loop", cause)); 188 | return; 189 | } else { 190 | String negotiateHeaderValue = null; 191 | final List headers = securityException.getHeader(KrbConstants.WWW_AUTHENTICATE); 192 | if (headers == null || headers.isEmpty()) { 193 | inner.onFailure(new ElasticsearchException("no auth header", cause)); 194 | return; 195 | } else if (headers.size() == 1) { 196 | negotiateHeaderValue = headers.get(0).trim(); 197 | } else { 198 | for (final String header : headers) { 199 | if (header != null && header.toLowerCase(Locale.ENGLISH).startsWith(KrbConstants.NEGOTIATE)) { 200 | negotiateHeaderValue = header.trim(); 201 | break; 202 | } 203 | } 204 | } 205 | 206 | if (negotiateHeaderValue == null) { 207 | inner.onFailure(new ElasticsearchException("no negotiate auth header")); 208 | return; 209 | } 210 | 211 | byte[] challenge = null; 212 | 213 | try { 214 | if (negotiateHeaderValue.length() > (KrbConstants.NEGOTIATE.length() + 1)) { 215 | challenge = DatatypeConverter 216 | .parseBase64Binary(negotiateHeaderValue.substring(KrbConstants.NEGOTIATE.length() + 2)); 217 | } 218 | 219 | byte[] data = null; 220 | 221 | if (challenge == null) { 222 | logger.debug("challenge is null"); 223 | data = context.initSecContext(new byte[0], 0, 0); 224 | request.putHeader("Authorization", "Negotiate " + DatatypeConverter.printBase64Binary(data)); 225 | 226 | } else { 227 | logger.debug("challenge is not null"); 228 | data = context.initSecContext(challenge, 0, challenge.length); 229 | request.putHeader("Authorization", "Negotiate " + DatatypeConverter.printBase64Binary(data)); 230 | addAdditionalHeader(request, count, data); 231 | } 232 | 233 | KerberizedClient.this.doExecute(action, request, this); 234 | 235 | } catch (final Exception e1) { 236 | inner.onFailure(e); 237 | return; 238 | } 239 | } 240 | } else { 241 | inner.onFailure(e); 242 | return; 243 | } 244 | } 245 | 246 | } 247 | 248 | void addAdditionalHeader(final ActionRequest request, final int count, final byte[] data) { 249 | 250 | } 251 | 252 | GSSContext initGSS() throws Exception { 253 | final GSSManager MANAGER = GSSManager.getInstance(); 254 | 255 | final PrivilegedExceptionAction action = new PrivilegedExceptionAction() { 256 | @Override 257 | public GSSCredential run() throws GSSException { 258 | return MANAGER.createCredential(null, GSSCredential.DEFAULT_LIFETIME, KrbConstants.SPNEGO, GSSCredential.INITIATE_ONLY); 259 | } 260 | }; 261 | 262 | final GSSCredential clientcreds = Subject.doAs(initiatorSubject, action); 263 | 264 | final GSSContext context = MANAGER.createContext(MANAGER.createName(acceptorPrincipal, GSSName.NT_USER_NAME, KrbConstants.SPNEGO), 265 | KrbConstants.SPNEGO, clientcreds, GSSContext.DEFAULT_LIFETIME); 266 | 267 | //TODO make configurable 268 | context.requestMutualAuth(true); 269 | context.requestConf(true); 270 | context.requestInteg(true); 271 | context.requestReplayDet(true); 272 | context.requestSequenceDet(true); 273 | context.requestCredDeleg(false); 274 | 275 | return context; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/test/java/de/codecentric/elasticsearch/plugin/kerberosrealm/AbstractUnitTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm; 19 | 20 | import static org.elasticsearch.common.settings.Settings.settingsBuilder; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.net.URISyntaxException; 25 | import java.net.URL; 26 | import java.nio.file.Files; 27 | import java.nio.file.Path; 28 | import java.nio.file.Paths; 29 | import java.security.Principal; 30 | import java.util.HashSet; 31 | import java.util.Iterator; 32 | import java.util.Set; 33 | 34 | import org.apache.commons.io.FileUtils; 35 | import org.apache.http.auth.AuthSchemeProvider; 36 | import org.apache.http.auth.AuthScope; 37 | import org.apache.http.auth.Credentials; 38 | import org.apache.http.auth.NTCredentials; 39 | import org.apache.http.client.CredentialsProvider; 40 | import org.apache.http.client.config.AuthSchemes; 41 | import org.apache.http.config.Registry; 42 | import org.apache.http.config.RegistryBuilder; 43 | import org.apache.http.config.SocketConfig; 44 | import org.apache.http.impl.auth.NTLMSchemeFactory; 45 | import org.apache.http.impl.auth.SPNegoSchemeFactory; 46 | import org.apache.http.impl.client.BasicCredentialsProvider; 47 | import org.apache.http.impl.client.CloseableHttpClient; 48 | import org.apache.http.impl.client.HttpClientBuilder; 49 | import org.apache.http.impl.client.HttpClients; 50 | import org.apache.kerby.util.NetworkUtil; 51 | import org.elasticsearch.ElasticsearchTimeoutException; 52 | import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; 53 | import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; 54 | import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; 55 | import org.elasticsearch.client.Client; 56 | import org.elasticsearch.cluster.health.ClusterHealthStatus; 57 | import org.elasticsearch.common.SuppressForbidden; 58 | import org.elasticsearch.common.logging.ESLogger; 59 | import org.elasticsearch.common.logging.Loggers; 60 | import org.elasticsearch.common.settings.Settings; 61 | import org.elasticsearch.common.unit.TimeValue; 62 | import org.elasticsearch.license.plugin.LicensePlugin; 63 | import org.elasticsearch.node.Node; 64 | import org.elasticsearch.node.NodeBuilder; 65 | import org.elasticsearch.node.PluginEnabledNode; 66 | import org.elasticsearch.shield.ShieldPlugin; 67 | import org.junit.After; 68 | import org.junit.Assert; 69 | import org.junit.Before; 70 | import org.junit.Rule; 71 | import org.junit.rules.TestName; 72 | import org.junit.rules.TestWatcher; 73 | import org.junit.runner.Description; 74 | 75 | import com.google.common.collect.Lists; 76 | 77 | import de.codecentric.elasticsearch.plugin.kerberosrealm.realm.KerberosRealm; 78 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.EmbeddedKRBServer; 79 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.JaasKrbUtil; 80 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.KrbConstants; 81 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.PropertyUtil; 82 | 83 | @SuppressForbidden(reason = "unit test") 84 | public abstract class AbstractUnitTest { 85 | 86 | public static boolean debugAll = false; 87 | protected static String PREFIX = "shield.authc.realms.cc-kerberos."; 88 | 89 | static { 90 | System.out.println("OS: " + System.getProperty("os.name") + " " + System.getProperty("os.arch") + " " 91 | + System.getProperty("os.version")); 92 | System.out.println("Java Version: " + System.getProperty("java.version") + " " + System.getProperty("java.vendor")); 93 | System.out.println("JVM Impl.: " + System.getProperty("java.vm.version") + " " + System.getProperty("java.vm.vendor") + " " 94 | + System.getProperty("java.vm.name")); 95 | 96 | if (debugAll) { 97 | System.setProperty("sun.security.krb5.debug", "true"); 98 | System.setProperty("java.security.debug", "all"); 99 | System.setProperty("sun.security.spnego.debug", "true"); 100 | System.setProperty("java.security.auth.debug", "all"); 101 | JaasKrbUtil.ENABLE_DEBUG = true; 102 | } 103 | } 104 | 105 | @Rule 106 | public TestName name = new TestName(); 107 | protected final String clustername = "kerberos_testcluster"; 108 | protected int elasticsearchHttpPort1; 109 | private int elasticsearchHttpPort2; 110 | private int elasticsearchHttpPort3; 111 | //public int elasticsearchNodePort1; 112 | //public int elasticsearchNodePort2; 113 | //public int elasticsearchNodePort3; 114 | 115 | private Node esNode1; 116 | private Node esNode2; 117 | private Node esNode3; 118 | private Client client; 119 | protected final ESLogger log = Loggers.getLogger(this.getClass()); 120 | protected final EmbeddedKRBServer embeddedKrbServer = new EmbeddedKRBServer(); 121 | 122 | @Rule 123 | public final TestWatcher testWatcher = new TestWatcher() { 124 | @Override 125 | protected void starting(final Description description) { 126 | final String methodName = description.getMethodName(); 127 | String className = description.getClassName(); 128 | className = className.substring(className.lastIndexOf('.') + 1); 129 | System.out.println("---------------- Starting JUnit-test: " + className + " " + methodName + " ----------------"); 130 | } 131 | 132 | @Override 133 | protected void failed(final Throwable e, final Description description) { 134 | final String methodName = description.getMethodName(); 135 | String className = description.getClassName(); 136 | className = className.substring(className.lastIndexOf('.') + 1); 137 | System.out.println(">>>> " + className + " " + methodName + " FAILED due to " + e); 138 | } 139 | 140 | @Override 141 | protected void finished(final Description description) { 142 | //System.out.println("-----------------------------------------------------------------------------------------"); 143 | } 144 | 145 | }; 146 | 147 | protected AbstractUnitTest() { 148 | super(); 149 | } 150 | 151 | private Settings.Builder getDefaultSettingsBuilder(final int nodenum, final int nodePort, final int httpPort, final boolean dataNode, 152 | final boolean masterNode) { 153 | 154 | // @formatter:off 155 | return settingsBuilder() 156 | //.putArray("plugin.types", ShieldPlugin.class.getName(), LicensePlugin.class.getName(), KerberosRealmPlugin.class.getName()) 157 | .putArray("plugin.mandatory",KerberosRealm.TYPE + "-realm","shield","license") 158 | .put("index.queries.cache.type", "opt_out_cache").put(PREFIX + "order", 0).put(PREFIX + "type", "cc-kerberos") 159 | .put("path.home", ".").put("node.name", "kerberosrealm_testnode_" + nodenum).put("node.data", dataNode) 160 | .put("node.master", masterNode).put("cluster.name", clustername).put("path.data", "testtmp/data") 161 | .put("path.work", "testtmp/work").put("path.logs", "testtmp/logs").put("path.conf", "testtmp/config") 162 | .put("path.plugins", "testtmp/plugins").put("index.number_of_shards", "2").put("index.number_of_replicas", "1") 163 | .put("http.host", "localhost") 164 | .put("http.port", httpPort) 165 | .put("http.enabled", !dataNode) 166 | //.put("transport.tcp.port", nodePort) //currently not working 167 | .put("http.cors.enabled", true) 168 | //.put("network.host", getNonLocalhostAddress()) //currently not working 169 | //.put("node.local", true); //do not use 170 | .put("node.local", false); 171 | // @formatter:on 172 | } 173 | 174 | protected final String getServerUri() { 175 | final String address = "http://localhost:" + elasticsearchHttpPort1; 176 | log.debug("Connect to {}", address); 177 | return address; 178 | } 179 | 180 | public final void startES(final Settings settings) throws Exception { 181 | FileUtils.copyFileToDirectory(getAbsoluteFilePathFromClassPath("roles.yml").toFile(), new File("testtmp/config/shield")); 182 | 183 | final Set ports = new HashSet<>(); 184 | do { 185 | ports.add(NetworkUtil.getServerPort()); 186 | } while (ports.size() < 7); 187 | 188 | final Iterator portIt = ports.iterator(); 189 | 190 | elasticsearchHttpPort1 = portIt.next(); 191 | elasticsearchHttpPort2 = portIt.next(); 192 | elasticsearchHttpPort3 = portIt.next(); 193 | 194 | //elasticsearchNodePort1 = portIt.next(); 195 | //elasticsearchNodePort2 = portIt.next(); 196 | //elasticsearchNodePort3 = portIt.next(); 197 | 198 | esNode1 = new PluginEnabledNode(getDefaultSettingsBuilder(1, 0, elasticsearchHttpPort1, false, true).put( 199 | settings == null ? Settings.Builder.EMPTY_SETTINGS : settings).build(), Lists.newArrayList(ShieldPlugin.class, LicensePlugin.class, KerberosRealmPlugin.class)).start(); 200 | client = esNode1.client(); 201 | 202 | esNode2 = new PluginEnabledNode(getDefaultSettingsBuilder(2, 0, elasticsearchHttpPort2, true, true).put( 203 | settings == null ? Settings.Builder.EMPTY_SETTINGS : settings).build(), Lists.newArrayList(ShieldPlugin.class, LicensePlugin.class, KerberosRealmPlugin.class)).start(); 204 | 205 | esNode3 = new PluginEnabledNode(getDefaultSettingsBuilder(3, 0, elasticsearchHttpPort3, true, false).put( 206 | settings == null ? Settings.Builder.EMPTY_SETTINGS : settings).build(), Lists.newArrayList(ShieldPlugin.class, LicensePlugin.class, KerberosRealmPlugin.class)).start(); 207 | 208 | waitForGreenClusterState(); 209 | final NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().get(); 210 | final NodeInfo[] nodes = nodeInfos.getNodes(); 211 | Assert.assertEquals(nodes + "", 3, nodes.length); 212 | } 213 | 214 | @Before 215 | public final void startKRBServer() throws Exception { 216 | FileUtils.deleteDirectory(new File("testtmp")); 217 | FileUtils.forceMkdir(new File("testtmp/tgtcc/")); 218 | FileUtils.forceMkdir(new File("testtmp/keytab/")); 219 | 220 | String loginconf = FileUtils.readFileToString(getAbsoluteFilePathFromClassPath("login.conf_template").toFile()); 221 | 222 | // @formatter:on 223 | loginconf = loginconf.replace("${debug}", String.valueOf(debugAll)).replace("${initiator.principal}", "spock/admin@CCK.COM") 224 | .replace("${initiator.ticketcache}", new File("testtmp/tgtcc/spock.cc").toURI().toString()) 225 | .replace("${keytab}", new File("testtmp/keytab/es_server.keytab").toURI().toString()); 226 | // @formatter:off 227 | 228 | final File loginconfFile = new File("testtmp/jaas/login.conf"); 229 | FileUtils.write(new File("testtmp/jaas/login.conf"), loginconf); 230 | PropertyUtil.setSystemProperty(KrbConstants.JAAS_LOGIN_CONF_PROP, loginconfFile.getAbsolutePath(), true); 231 | 232 | embeddedKrbServer.start(new File("testtmp/simplekdc/")); 233 | 234 | FileUtils.copyFileToDirectory(new File("testtmp/simplekdc/krb5.conf"), new File("testtmp/config/data/simplekdc/")); 235 | } 236 | 237 | @After 238 | public void tearDown() throws Exception { 239 | if (esNode3 != null) { 240 | esNode3.close(); 241 | } 242 | 243 | if (esNode2 != null) { 244 | esNode2.close(); 245 | } 246 | 247 | if (esNode1 != null) { 248 | esNode1.close(); 249 | } 250 | 251 | if (client != null) { 252 | client.close(); 253 | } 254 | 255 | if (embeddedKrbServer != null) { 256 | embeddedKrbServer.getSimpleKdcServer().stop(); 257 | } 258 | 259 | } 260 | 261 | protected final CloseableHttpClient getHttpClient(final boolean useSpnego) throws Exception { 262 | 263 | final CredentialsProvider credsProvider = new BasicCredentialsProvider(); 264 | final HttpClientBuilder hcb = HttpClients.custom(); 265 | 266 | if (useSpnego) { 267 | //SPNEGO/Kerberos setup 268 | log.debug("SPNEGO activated"); 269 | final AuthSchemeProvider nsf = new SPNegoSchemeFactory(true);// new NegotiateSchemeProvider(); 270 | final Credentials jaasCreds = new JaasCredentials(); 271 | credsProvider.setCredentials(new AuthScope(null, -1, null, AuthSchemes.SPNEGO), jaasCreds); 272 | credsProvider.setCredentials(new AuthScope(null, -1, null, AuthSchemes.NTLM), new NTCredentials("Guest", "Guest", "Guest", 273 | "Guest")); 274 | final Registry authSchemeRegistry = RegistryBuilder. create() 275 | .register(AuthSchemes.SPNEGO, nsf).register(AuthSchemes.NTLM, new NTLMSchemeFactory()).build(); 276 | 277 | hcb.setDefaultAuthSchemeRegistry(authSchemeRegistry); 278 | } 279 | 280 | hcb.setDefaultCredentialsProvider(credsProvider); 281 | hcb.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(10 * 1000).build()); 282 | final CloseableHttpClient httpClient = hcb.build(); 283 | return httpClient; 284 | } 285 | 286 | protected void waitForGreenClusterState() throws IOException { 287 | waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(30)); 288 | } 289 | 290 | protected void waitForCluster(final ClusterHealthStatus status, final TimeValue timeout) throws IOException { 291 | try { 292 | log.debug("waiting for cluster state {}", status.name()); 293 | final ClusterHealthResponse healthResponse = client.admin().cluster().prepareHealth().setWaitForStatus(status) 294 | .setWaitForNodes(">2").setTimeout(timeout).execute().actionGet(); 295 | if (healthResponse.isTimedOut()) { 296 | throw new IOException("cluster state is " + healthResponse.getStatus().name() + " and not " + status.name() 297 | + ", cowardly refusing to continue with operations"); 298 | } else { 299 | log.debug("... cluster state ok"); 300 | } 301 | } catch (final ElasticsearchTimeoutException e) { 302 | throw new IOException("timeout, cluster does not respond to health request, cowardly refusing to continue with operations"); 303 | } 304 | } 305 | 306 | private static class JaasCredentials implements Credentials { 307 | 308 | @Override 309 | public String getPassword() { 310 | return null; 311 | } 312 | 313 | @Override 314 | public Principal getUserPrincipal() { 315 | return null; 316 | } 317 | } 318 | 319 | protected Client client() { 320 | return client; 321 | } 322 | 323 | private Path getAbsoluteFilePathFromClassPath(final String fileNameFromClasspath) { 324 | Path path = null; 325 | final URL fileUrl = PropertyUtil.class.getClassLoader().getResource(fileNameFromClasspath); 326 | if (fileUrl != null) { 327 | try { 328 | path = Paths.get(fileUrl.toURI()); 329 | if (!Files.isReadable(path) && !Files.isDirectory(path)) { 330 | log.error("Cannot read from {}, file does not exist or is not readable", path.toString()); 331 | return null; 332 | } 333 | 334 | if (!path.isAbsolute()) { 335 | log.warn("{} is not absolute", path.toString()); 336 | } 337 | return path; 338 | } catch (final URISyntaxException e) { 339 | //ignore 340 | } 341 | } else { 342 | log.error("Failed to load " + fileNameFromClasspath); 343 | } 344 | return null; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosRealm.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | and Apache Tomcat project https://tomcat.apache.org/ (see comments and NOTICE) 18 | */ 19 | package de.codecentric.elasticsearch.plugin.kerberosrealm.realm; 20 | 21 | import java.io.Serializable; 22 | import java.nio.charset.StandardCharsets; 23 | import java.nio.file.Files; 24 | import java.nio.file.Path; 25 | import java.security.Principal; 26 | import java.security.PrivilegedAction; 27 | import java.security.PrivilegedActionException; 28 | import java.security.PrivilegedExceptionAction; 29 | import java.util.Arrays; 30 | import java.util.List; 31 | import java.util.Locale; 32 | import java.util.Map; 33 | 34 | import javax.security.auth.Subject; 35 | import javax.security.auth.login.LoginException; 36 | import javax.xml.bind.DatatypeConverter; 37 | 38 | import org.elasticsearch.ElasticsearchException; 39 | import org.elasticsearch.ExceptionsHelper; 40 | import org.elasticsearch.action.admin.cluster.node.liveness.LivenessRequest; 41 | import org.elasticsearch.common.logging.ESLogger; 42 | import org.elasticsearch.common.settings.Settings; 43 | import org.elasticsearch.env.Environment; 44 | import org.elasticsearch.rest.RestRequest; 45 | import org.elasticsearch.shield.InternalSystemUser; 46 | import org.elasticsearch.shield.User; 47 | import org.elasticsearch.shield.authc.AuthenticationToken; 48 | import org.elasticsearch.shield.authc.Realm; 49 | import org.elasticsearch.shield.authc.RealmConfig; 50 | import org.elasticsearch.transport.TransportMessage; 51 | import org.ietf.jgss.GSSContext; 52 | import org.ietf.jgss.GSSCredential; 53 | import org.ietf.jgss.GSSException; 54 | import org.ietf.jgss.GSSManager; 55 | import org.ietf.jgss.GSSName; 56 | 57 | import com.google.common.collect.ArrayListMultimap; 58 | import com.google.common.collect.Iterators; 59 | import com.google.common.collect.ListMultimap; 60 | 61 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.JaasKrbUtil; 62 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.KrbConstants; 63 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.SettingConstants; 64 | 65 | /** 66 | */ 67 | public class KerberosRealm extends Realm { 68 | 69 | public static final String TYPE = "cc-kerberos"; 70 | 71 | private final boolean stripRealmFromPrincipalName; 72 | private final String acceptorPrincipal; 73 | private final Path acceptorKeyTabPath; 74 | private final ListMultimap rolesMap = ArrayListMultimap. create(); 75 | private final Environment env; 76 | private final boolean mockMode; 77 | 78 | public KerberosRealm(final RealmConfig config) { 79 | super(TYPE, config); 80 | stripRealmFromPrincipalName = config.settings().getAsBoolean(SettingConstants.STRIP_REALM_FROM_PRINCIPAL, true); 81 | acceptorPrincipal = config.settings().get(SettingConstants.ACCEPTOR_PRINCIPAL, null); 82 | final String acceptorKeyTab = config.settings().get(SettingConstants.ACCEPTOR_KEYTAB_PATH, null); 83 | 84 | //shield.authc.realms.cc-kerberos.roles.: principal1, principal2 85 | //shield.authc.realms.cc-kerberos.roles.: principal1, principal3 86 | ////shield.authc.realms.cc-kerberos.roles.admin: luke@EXAMPLE.COM, vader@EXAMPLE.COM 87 | 88 | Map roleGroups = config.settings().getGroups(SettingConstants.ROLES+"."); 89 | 90 | if(roleGroups != null) { 91 | for(String roleGroup:roleGroups.keySet()) { 92 | 93 | for(String principal:config.settings().getAsArray(SettingConstants.ROLES+"."+roleGroup)) { 94 | rolesMap.put(stripRealmName(principal, stripRealmFromPrincipalName), roleGroup); 95 | } 96 | } 97 | } 98 | 99 | logger.debug("Parsed roles: {}", rolesMap); 100 | 101 | env = new Environment(config.globalSettings()); 102 | mockMode = config.settings().getAsBoolean("mock_mode", false); 103 | 104 | if (acceptorPrincipal == null) { 105 | throw new ElasticsearchException("Unconfigured (but required) property: {}", SettingConstants.ACCEPTOR_PRINCIPAL); 106 | } 107 | 108 | if (acceptorKeyTab == null) { 109 | throw new ElasticsearchException("Unconfigured (but required) property: {}", SettingConstants.ACCEPTOR_KEYTAB_PATH); 110 | } 111 | 112 | acceptorKeyTabPath = env.configFile().resolve(acceptorKeyTab); 113 | 114 | if (!mockMode && (!Files.isReadable(acceptorKeyTabPath) && !Files.isDirectory(acceptorKeyTabPath))) { 115 | throw new ElasticsearchException("File not found or not readable: {}", acceptorKeyTabPath.toAbsolutePath()); 116 | } 117 | } 118 | 119 | /*protected KerberosRealm(final String type, final RealmConfig config) { 120 | this(config); 121 | }*/ 122 | 123 | @Override 124 | public boolean supports(final AuthenticationToken token) { 125 | return token instanceof KerberosAuthenticationToken; 126 | } 127 | 128 | @Override 129 | public KerberosAuthenticationToken token(final RestRequest request) { 130 | if (logger.isDebugEnabled()) { 131 | logger.debug("Rest request headers: {}", Iterators.toString(request.headers().iterator())); 132 | } 133 | final String authorizationHeader = request.header("Authorization"); 134 | final KerberosAuthenticationToken token = token(authorizationHeader); 135 | if (token != null && logger.isDebugEnabled()) { 136 | logger.debug("Rest request token '{}' for {} successully generated", token, request.path()); 137 | } 138 | return token; 139 | } 140 | 141 | private KerberosAuthenticationToken token(final String authorizationHeader) { 142 | if (mockMode) { 143 | return tokenMock(authorizationHeader); 144 | } else { 145 | return tokenKerb(authorizationHeader); 146 | } 147 | } 148 | 149 | private KerberosAuthenticationToken tokenMock(final String authorizationHeader) { 150 | //Negotiate YYYYVVV.... 151 | //Negotiate_c YYYYVVV.... 152 | 153 | if (authorizationHeader != null && acceptorPrincipal != null) { 154 | 155 | if (!authorizationHeader.trim().toLowerCase(Locale.ENGLISH).startsWith("negotiate")) { 156 | throw new ElasticsearchException("Bad 'Authorization' header"); 157 | } else { 158 | if (authorizationHeader.trim().toLowerCase(Locale.ENGLISH).startsWith("negotiate_c")) { 159 | //client indicates that this is the last round of security context establishment 160 | return new KerberosAuthenticationToken("finaly negotiate token".getBytes(StandardCharsets.UTF_8), "mock_principal"); 161 | } else { 162 | //client want another ound of security context establishment 163 | final ElasticsearchException ee = new ElasticsearchException("MOCK TEST EXCEPTION"); 164 | ee.addHeader("kerberos_out_token", "mocked non _c negotiate"); 165 | throw ee; 166 | } 167 | } 168 | 169 | } 170 | 171 | return null; 172 | } 173 | 174 | private KerberosAuthenticationToken tokenKerb(final String authorizationHeader) { 175 | Principal principal = null; 176 | 177 | if (authorizationHeader != null && acceptorKeyTabPath != null && acceptorPrincipal != null) { 178 | 179 | if (!authorizationHeader.trim().toLowerCase(Locale.ENGLISH).startsWith("negotiate ")) { 180 | throw new ElasticsearchException("Bad 'Authorization' header"); 181 | } else { 182 | 183 | final byte[] decodedNegotiateHeader = DatatypeConverter.parseBase64Binary(authorizationHeader.substring(10)); 184 | 185 | GSSContext gssContext = null; 186 | byte[] outToken = null; 187 | 188 | try { 189 | 190 | final Subject subject = JaasKrbUtil.loginUsingKeytab(acceptorPrincipal, acceptorKeyTabPath, false); 191 | 192 | final GSSManager manager = GSSManager.getInstance(); 193 | final int credentialLifetime = GSSCredential.INDEFINITE_LIFETIME; 194 | 195 | final PrivilegedExceptionAction action = new PrivilegedExceptionAction() { 196 | @Override 197 | public GSSCredential run() throws GSSException { 198 | return manager.createCredential(null, credentialLifetime, KrbConstants.SPNEGO, GSSCredential.ACCEPT_ONLY); 199 | } 200 | }; 201 | gssContext = manager.createContext(Subject.doAs(subject, action)); 202 | 203 | outToken = Subject.doAs(subject, new AcceptAction(gssContext, decodedNegotiateHeader)); 204 | 205 | if (outToken == null) { 206 | logger.warn("Ticket validation not successful, outToken is null"); 207 | return null; 208 | } 209 | 210 | principal = Subject.doAs(subject, new AuthenticateAction(logger, gssContext, stripRealmFromPrincipalName)); 211 | 212 | } catch (final LoginException e) { 213 | logger.error("Login exception due to {}", e, e.toString()); 214 | throw ExceptionsHelper.convertToRuntime(e); 215 | } catch (final GSSException e) { 216 | logger.error("Ticket validation not successful due to {}", e, e.toString()); 217 | throw ExceptionsHelper.convertToRuntime(e); 218 | } catch (final PrivilegedActionException e) { 219 | final Throwable cause = e.getCause(); 220 | if (cause instanceof GSSException) { 221 | logger.warn("Service login not successful due to {}", e, e.toString()); 222 | } else { 223 | logger.error("Service login not successful due to {}", e, e.toString()); 224 | } 225 | throw ExceptionsHelper.convertToRuntime(e); 226 | } finally { 227 | if (gssContext != null) { 228 | try { 229 | gssContext.dispose(); 230 | } catch (final GSSException e) { 231 | // Ignore 232 | } 233 | } 234 | //TODO subject logout 235 | } 236 | 237 | if (principal == null) { 238 | final ElasticsearchException ee = new ElasticsearchException("Principal null"); 239 | ee.addHeader("kerberos_out_token", DatatypeConverter.printBase64Binary(outToken)); 240 | throw ee; 241 | } 242 | 243 | final String username = ((SimpleUserPrincipal) principal).getName(); 244 | return new KerberosAuthenticationToken(outToken, username); 245 | } 246 | 247 | } else { 248 | return null; 249 | } 250 | } 251 | 252 | @Override 253 | public KerberosAuthenticationToken token(final TransportMessage message) { 254 | 255 | if (logger.isDebugEnabled()) { 256 | logger.debug("Transport request headers: {}", message.getHeaders()); 257 | } 258 | 259 | if (message instanceof LivenessRequest) { 260 | return KerberosAuthenticationToken.LIVENESS_TOKEN; 261 | } 262 | 263 | final String authorizationHeader = message.getHeader("Authorization"); 264 | final KerberosAuthenticationToken token = token(authorizationHeader); 265 | if (token != null && logger.isDebugEnabled()) { 266 | logger.debug("Transport message token '{}' for message {} successully generated", token, message.getClass()); 267 | } 268 | return token; 269 | } 270 | 271 | @Override 272 | public User authenticate(final KerberosAuthenticationToken token) { 273 | 274 | if(token == KerberosAuthenticationToken.LIVENESS_TOKEN) { 275 | return InternalSystemUser.INSTANCE; 276 | } 277 | 278 | final String actualUser = token.principal(); 279 | 280 | if (actualUser == null || actualUser.isEmpty() || token.credentials() == null) { 281 | logger.warn("User '{}' cannot be authenticated", actualUser); 282 | return null; 283 | } 284 | 285 | String[] userRoles = new String[0]; 286 | List userRolesList = rolesMap.get(actualUser); 287 | 288 | if(userRolesList != null && !userRolesList.isEmpty()) { 289 | userRoles = userRolesList.toArray(new String[0]); 290 | } 291 | 292 | logger.debug("User '{}' with roles {} successully authenticated", actualUser, Arrays.toString(userRoles)); 293 | return new User(actualUser, userRoles); 294 | } 295 | 296 | @Override 297 | public User lookupUser(final String username) { 298 | return null; 299 | } 300 | 301 | @Override 302 | public boolean userLookupSupported() { 303 | return false; 304 | } 305 | 306 | /** 307 | * This class gets a gss credential via a privileged action. 308 | */ 309 | //borrowed from Apache Tomcat 8 http://svn.apache.org/repos/asf/tomcat/tc8.0.x/trunk/ 310 | private static class AcceptAction implements PrivilegedExceptionAction { 311 | 312 | GSSContext gssContext; 313 | 314 | byte[] decoded; 315 | 316 | AcceptAction(final GSSContext context, final byte[] decodedToken) { 317 | this.gssContext = context; 318 | this.decoded = decodedToken; 319 | } 320 | 321 | @Override 322 | public byte[] run() throws GSSException { 323 | return gssContext.acceptSecContext(decoded, 0, decoded.length); 324 | } 325 | } 326 | 327 | //borrowed from Apache Tomcat 8 http://svn.apache.org/repos/asf/tomcat/tc8.0.x/trunk/ 328 | private static class AuthenticateAction implements PrivilegedAction { 329 | 330 | private final ESLogger logger; 331 | private final GSSContext gssContext; 332 | private final boolean strip; 333 | 334 | private AuthenticateAction(final ESLogger logger, final GSSContext gssContext, final boolean strip) { 335 | super(); 336 | this.logger = logger; 337 | this.gssContext = gssContext; 338 | this.strip = strip; 339 | } 340 | 341 | @Override 342 | public Principal run() { 343 | return new SimpleUserPrincipal(getUsernameFromGSSContext(gssContext, strip, logger)); 344 | } 345 | } 346 | 347 | //borrowed from Apache Tomcat 8 http://svn.apache.org/repos/asf/tomcat/tc8.0.x/trunk/ 348 | private static String getUsernameFromGSSContext(final GSSContext gssContext, final boolean strip, final ESLogger logger) { 349 | if (gssContext.isEstablished()) { 350 | GSSName gssName = null; 351 | try { 352 | gssName = gssContext.getSrcName(); 353 | } catch (final GSSException e) { 354 | logger.error("Unable to get src name from gss context", e); 355 | } 356 | 357 | if (gssName != null) { 358 | String name = gssName.toString(); 359 | 360 | return stripRealmName(name, strip); 361 | 362 | } 363 | } 364 | 365 | return null; 366 | } 367 | 368 | private static String stripRealmName(String name, boolean strip){ 369 | if (strip && name != null) { 370 | final int i = name.indexOf('@'); 371 | if (i > 0) { 372 | // Zero so we don;t leave a zero length name 373 | name = name.substring(0, i); 374 | } 375 | } 376 | 377 | return name; 378 | } 379 | 380 | private static class SimpleUserPrincipal implements Principal, Serializable { 381 | 382 | private static final long serialVersionUID = -1; 383 | private final String username; 384 | 385 | SimpleUserPrincipal(final String username) { 386 | super(); 387 | this.username = username; 388 | } 389 | 390 | @Override 391 | public int hashCode() { 392 | final int prime = 31; 393 | int result = 1; 394 | result = prime * result + ((username == null) ? 0 : username.hashCode()); 395 | return result; 396 | } 397 | 398 | @Override 399 | public boolean equals(final Object obj) { 400 | if (this == obj) { 401 | return true; 402 | } 403 | if (obj == null) { 404 | return false; 405 | } 406 | if (getClass() != obj.getClass()) { 407 | return false; 408 | } 409 | final SimpleUserPrincipal other = (SimpleUserPrincipal) obj; 410 | if (username == null) { 411 | if (other.username != null) { 412 | return false; 413 | } 414 | } else if (!username.equals(other.username)) { 415 | return false; 416 | } 417 | return true; 418 | } 419 | 420 | @Override 421 | public String getName() { 422 | return this.username; 423 | } 424 | 425 | @Override 426 | public String toString() { 427 | final StringBuilder buffer = new StringBuilder(); 428 | buffer.append("[principal: "); 429 | buffer.append(this.username); 430 | buffer.append("]"); 431 | return buffer.toString(); 432 | } 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /src/test/java/de/codecentric/elasticsearch/plugin/kerberosrealm/KerberosRealmEmbeddedTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 codecentric AG 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: Hendrik Saly 17 | */ 18 | package de.codecentric.elasticsearch.plugin.kerberosrealm; 19 | 20 | import static org.hamcrest.Matchers.is; 21 | import static org.junit.Assert.assertThat; 22 | import static org.junit.Assert.assertTrue; 23 | import static org.junit.Assert.assertFalse; 24 | 25 | import java.io.File; 26 | import java.net.URL; 27 | 28 | import javax.security.auth.login.LoginException; 29 | 30 | import net.sourceforge.spnego.SpnegoHttpURLConnection; 31 | 32 | import org.apache.commons.io.FileUtils; 33 | import org.apache.http.client.methods.CloseableHttpResponse; 34 | import org.apache.http.client.methods.HttpGet; 35 | import org.apache.http.impl.client.CloseableHttpClient; 36 | import org.apache.kerby.kerberos.kerb.spec.ticket.TgtTicket; 37 | import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; 38 | import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; 39 | import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; 40 | import org.elasticsearch.client.transport.TransportClient; 41 | import org.elasticsearch.cluster.health.ClusterHealthStatus; 42 | import org.elasticsearch.common.SuppressForbidden; 43 | import org.elasticsearch.common.settings.Settings; 44 | import org.elasticsearch.common.xcontent.XContentBuilder; 45 | import org.elasticsearch.common.xcontent.XContentParser; 46 | import org.elasticsearch.common.xcontent.json.JsonXContent; 47 | import org.elasticsearch.rest.RestStatus; 48 | import org.elasticsearch.shield.ShieldPlugin; 49 | import org.junit.Assert; 50 | import org.junit.Ignore; 51 | import org.junit.Test; 52 | 53 | import de.codecentric.elasticsearch.plugin.kerberosrealm.client.KerberizedClient; 54 | import de.codecentric.elasticsearch.plugin.kerberosrealm.client.MockingKerberizedClient; 55 | import de.codecentric.elasticsearch.plugin.kerberosrealm.realm.KerberosRealm; 56 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.PropertyUtil; 57 | import de.codecentric.elasticsearch.plugin.kerberosrealm.support.SettingConstants; 58 | 59 | /** 60 | * Integration test to test authentication with the custom realm. This test is run against an external cluster that is launched 61 | * by maven and this test is not expected to run within an IDE. 62 | */ 63 | 64 | @SuppressForbidden(reason = "unit test") 65 | public class KerberosRealmEmbeddedTests extends AbstractUnitTest { 66 | 67 | @Test 68 | public void testTransportClient() throws Exception { 69 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("spock/admin@CCK.COM", "secret"); 70 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("elasticsearch/transport@CCK.COM", "testpwd"); 71 | FileUtils.forceMkdir(new File("testtmp/config/keytab/")); 72 | embeddedKrbServer.getSimpleKdcServer().exportPrincipal("elasticsearch/transport@CCK.COM", 73 | new File("testtmp/config/keytab/es_server.keytab")); //server, acceptor 74 | 75 | final Settings esServerSettings = Settings.builder() 76 | .put(PREFIX + SettingConstants.ACCEPTOR_KEYTAB_PATH, "keytab/es_server.keytab") 77 | //relative to config 78 | .put(PREFIX + SettingConstants.ACCEPTOR_PRINCIPAL, "elasticsearch/transport@CCK.COM") 79 | .put(PREFIX + SettingConstants.STRIP_REALM_FROM_PRINCIPAL, true) 80 | .putArray(PREFIX + SettingConstants.ROLES+".cc_kerberos_realm_role", "spock/admin@CCK.COM") 81 | //.put(PREFIX+SettingConstants.KRB5_FILE_PATH,"") //if already set by kerby here 82 | //.put(PREFIX+SettingConstants.KRB_DEBUG, true) 83 | .build(); 84 | 85 | this.startES(esServerSettings); 86 | 87 | final NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().get(); 88 | final NodeInfo[] nodes = nodeInfos.getNodes(); 89 | assertTrue(nodes.length > 2); 90 | 91 | final Settings settings = Settings.builder().put("cluster.name", clustername) 92 | .putArray("plugin.types", ShieldPlugin.class.getName()).build(); 93 | 94 | try (TransportClient client = TransportClient.builder().settings(settings).build()) { 95 | client.addTransportAddress(nodes[0].getTransport().address().publishAddress()); 96 | try (KerberizedClient kc = new KerberizedClient(client, "spock/admin@CCK.COM", "secret", "elasticsearch/transport@CCK.COM")) { 97 | 98 | ClusterHealthResponse response = kc.admin().cluster().prepareHealth().execute().actionGet(); 99 | assertThat(response.isTimedOut(), is(false)); 100 | 101 | response = kc.admin().cluster().prepareHealth().execute().actionGet(); 102 | assertThat(response.isTimedOut(), is(false)); 103 | 104 | response = kc.admin().cluster().prepareHealth().execute().actionGet(); 105 | assertThat(response.isTimedOut(), is(false)); 106 | assertThat(response.status(), is(RestStatus.OK)); 107 | assertThat(response.getStatus(), is(ClusterHealthStatus.GREEN)); 108 | } 109 | } 110 | } 111 | 112 | @Test 113 | public void testTransportClientMultiRound() throws Exception { 114 | 115 | //Mock mode, no kerberos involved 116 | 117 | embeddedKrbServer.getSimpleKdcServer().stop(); 118 | 119 | final Settings esServerSettings = Settings.builder().put(PREFIX + SettingConstants.ACCEPTOR_KEYTAB_PATH, "mock") 120 | .put(PREFIX + SettingConstants.ACCEPTOR_PRINCIPAL, "mock").put(PREFIX + "mock_mode", true) 121 | .putArray(PREFIX + SettingConstants.ROLES+".cc_kerberos_realm_role", "spock/admin@CCK.COM","mock_principal") 122 | .build(); 123 | 124 | this.startES(esServerSettings); 125 | 126 | final NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().get(); 127 | final NodeInfo[] nodes = nodeInfos.getNodes(); 128 | assertTrue(nodes.length > 2); 129 | 130 | final Settings settings = Settings.builder().put("cluster.name", clustername) 131 | .putArray("plugin.types", ShieldPlugin.class.getName()).build(); 132 | 133 | try (TransportClient client = TransportClient.builder().settings(settings).build()) { 134 | client.addTransportAddress(nodes[0].getTransport().address().publishAddress()); 135 | try (KerberizedClient kc = new MockingKerberizedClient(client)) { 136 | 137 | ClusterHealthResponse response = kc.admin().cluster().prepareHealth().execute().actionGet(); 138 | assertThat(response.isTimedOut(), is(false)); 139 | 140 | response = kc.admin().cluster().prepareHealth().execute().actionGet(); 141 | assertThat(response.isTimedOut(), is(false)); 142 | 143 | response = kc.admin().cluster().prepareHealth().execute().actionGet(); 144 | assertThat(response.isTimedOut(), is(false)); 145 | assertThat(response.status(), is(RestStatus.OK)); 146 | assertThat(response.getStatus(), is(ClusterHealthStatus.GREEN)); 147 | } 148 | } 149 | } 150 | 151 | @Test(expected = LoginException.class) 152 | public void testTransportClientBadUser() throws Exception { 153 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("spock/admin@CCK.COM", "secret"); 154 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("elasticsearch/transport@CCK.COM", "testpwd"); 155 | FileUtils.forceMkdir(new File("testtmp/config/keytab/")); 156 | embeddedKrbServer.getSimpleKdcServer().exportPrincipal("elasticsearch/transport@CCK.COM", 157 | new File("testtmp/config/keytab/es_server.keytab")); //server, acceptor 158 | 159 | final Settings esServerSettings = Settings.builder().put(PREFIX + SettingConstants.ACCEPTOR_KEYTAB_PATH, "keytab/es_server.keytab") 160 | .put(PREFIX + SettingConstants.ACCEPTOR_PRINCIPAL, "elasticsearch/transport@CCK.COM") 161 | .put(PREFIX + SettingConstants.STRIP_REALM_FROM_PRINCIPAL, true) 162 | .putArray(PREFIX + SettingConstants.ROLES+".cc_kerberos_realm_role", "spock/admin@CCK.COM") 163 | //.put(PREFIX+SettingConstants.KRB5_FILE_PATH,"") //if already set by kerby here 164 | //.put(PREFIX+SettingConstants.KRB_DEBUG, true) 165 | .build(); 166 | 167 | this.startES(esServerSettings); 168 | 169 | final NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().get(); 170 | final NodeInfo[] nodes = nodeInfos.getNodes(); 171 | assertTrue(nodes.length > 2); 172 | 173 | final Settings settings = Settings.builder().put("cluster.name", clustername) 174 | .putArray("plugin.types", ShieldPlugin.class.getName()).build(); 175 | 176 | try (TransportClient client = TransportClient.builder().settings(settings).build()) { 177 | client.addTransportAddress(nodes[0].getTransport().address().publishAddress()); 178 | try (KerberizedClient kc = new KerberizedClient(client, "spock/admin@CCK.COM_bad", "secret-wrong", 179 | "elasticsearch/transport@CCK.COM")) { 180 | 181 | ClusterHealthResponse response = kc.admin().cluster().prepareHealth().execute().actionGet(); 182 | assertThat(response.isTimedOut(), is(false)); 183 | 184 | response = kc.admin().cluster().prepareHealth().execute().actionGet(); 185 | assertThat(response.isTimedOut(), is(false)); 186 | 187 | response = kc.admin().cluster().prepareHealth().execute().actionGet(); 188 | assertThat(response.isTimedOut(), is(false)); 189 | assertThat(response.status(), is(RestStatus.OK)); 190 | assertThat(response.getStatus(), is(ClusterHealthStatus.GREEN)); 191 | } 192 | } 193 | } 194 | 195 | @Test 196 | public void testSettingsFiltering() throws Exception { 197 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("spock/admin@CCK.COM", "secret"); 198 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("HTTP/localhost@CCK.COM", "testpwd1"); 199 | 200 | FileUtils.forceMkdir(new File("testtmp/config/keytab/")); 201 | 202 | embeddedKrbServer.getSimpleKdcServer().exportPrincipal("HTTP/localhost@CCK.COM", 203 | new File("testtmp/config/keytab/es_server.keytab")); //server, acceptor 204 | 205 | final TgtTicket tgt = embeddedKrbServer.getSimpleKdcServer().getKrbClient().requestTgtWithPassword("spock/admin@CCK.COM", "secret"); 206 | embeddedKrbServer.getSimpleKdcServer().getKrbClient().storeTicket(tgt, new File("testtmp/tgtcc/spock.cc")); 207 | 208 | final Settings esServerSettings = Settings.builder().put(PREFIX + SettingConstants.ACCEPTOR_KEYTAB_PATH, "keytab/es_server.keytab") 209 | .put(PREFIX + SettingConstants.ACCEPTOR_PRINCIPAL, "HTTP/localhost@CCK.COM") 210 | .put(PREFIX + SettingConstants.STRIP_REALM_FROM_PRINCIPAL, true) 211 | .putArray(PREFIX + SettingConstants.ROLES+".cc_kerberos_realm_role", "spock/admin@CCK.COM") 212 | //.put(PREFIX+SettingConstants.KRB5_FILE_PATH,"") //if already set by kerby here 213 | //.put(PREFIX+SettingConstants.KRB_DEBUG, true) 214 | .build(); 215 | 216 | this.startES(esServerSettings); 217 | 218 | net.sourceforge.spnego.SpnegoHttpURLConnection hcon = new SpnegoHttpURLConnection("com.sun.security.jgss.krb5.initiate"); 219 | 220 | hcon.requestCredDeleg(true); 221 | hcon.connect(new URL(getServerUri() + "/_cluster/health")); 222 | Assert.assertEquals(200, hcon.getResponseCode()); 223 | 224 | hcon = new SpnegoHttpURLConnection("com.sun.security.jgss.krb5.initiate"); 225 | hcon.requestCredDeleg(true); 226 | hcon.connect(new URL(getServerUri() + "/_nodes/settings")); 227 | Assert.assertEquals(200, hcon.getResponseCode()); 228 | 229 | //final CloseableHttpClient httpClient = getHttpClient(true); 230 | //final CloseableHttpResponse response = httpClient.execute(new HttpGet(new URL(getServerUri() + "/_nodes/settings").toURI())); 231 | 232 | //assertThat(response.getStatusLine().getStatusCode(), is(200)); 233 | 234 | final XContentParser parser = JsonXContent.jsonXContent.createParser(hcon.getInputStream()); 235 | XContentParser.Token token; 236 | Settings settings = null; 237 | while ((token = parser.nextToken()) != null) { 238 | if (token == XContentParser.Token.FIELD_NAME && parser.currentName().equals("settings")) { 239 | parser.nextToken(); 240 | final XContentBuilder builder = XContentBuilder.builder(parser.contentType().xContent()); 241 | settings = Settings.builder().loadFromSource(builder.copyCurrentStructure(parser).bytes().toUtf8()).build(); 242 | break; 243 | } 244 | } 245 | assertTrue(settings != null); 246 | assertFalse(settings.getAsMap().isEmpty()); 247 | assertTrue(settings.getGroups("shield.authc.realms." + KerberosRealm.TYPE).isEmpty()); 248 | } 249 | 250 | @Test 251 | public void testRestNoTicketCache() throws Exception { 252 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("spock/admin@CCK.COM", "secret"); 253 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("HTTP/localhost@CCK.COM", "testpwd1"); 254 | FileUtils.forceMkdir(new File("testtmp/config/keytab/")); 255 | embeddedKrbServer.getSimpleKdcServer().exportPrincipal("HTTP/localhost@CCK.COM", 256 | new File("testtmp/config/keytab/es_server.keytab")); //server, acceptor 257 | 258 | final Settings esServerSettings = Settings.builder().put(PREFIX + SettingConstants.ACCEPTOR_KEYTAB_PATH, "keytab/es_server.keytab") 259 | .put(PREFIX + SettingConstants.ACCEPTOR_PRINCIPAL, "HTTP/localhost@CCK.COM") 260 | .put(PREFIX + SettingConstants.STRIP_REALM_FROM_PRINCIPAL, true) 261 | .putArray(PREFIX + SettingConstants.ROLES+".cc_kerberos_realm_role", "spock/admin@CCK.COM") 262 | //.put(PREFIX+SettingConstants.KRB5_FILE_PATH,"") //if already set by kerby here 263 | //.put(PREFIX+SettingConstants.KRB_DEBUG, true) 264 | .build(); 265 | 266 | this.startES(esServerSettings); 267 | 268 | net.sourceforge.spnego.SpnegoHttpURLConnection hcon = new SpnegoHttpURLConnection("no.ticket.cache","spock/admin@CCK.COM","secret"); 269 | 270 | hcon.requestCredDeleg(true); 271 | hcon.connect(new URL(getServerUri() + "/_nodes/settings")); 272 | Assert.assertEquals(200, hcon.getResponseCode()); 273 | 274 | //final CloseableHttpClient httpClient = getHttpClient(true); 275 | //final CloseableHttpResponse response = httpClient.execute(new HttpGet(new URL(getServerUri() + "/_nodes/settings").toURI())); 276 | 277 | //assertThat(response.getStatusLine().getStatusCode(), is(401)); 278 | } 279 | 280 | @Test 281 | @Ignore 282 | public void testRestNoTicket() throws Exception { 283 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("spock/admin@CCK.COM", "secret"); 284 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("HTTP/localhost@CCK.COM", "testpwd1"); 285 | FileUtils.forceMkdir(new File("testtmp/config/keytab/")); 286 | embeddedKrbServer.getSimpleKdcServer().exportPrincipal("HTTP/localhost@CCK.COM", 287 | new File("testtmp/config/keytab/es_server.keytab")); //server, acceptor 288 | 289 | //final TgtTicket tgt = embeddedKrbServer.getSimpleKdcServer().getKrbClient().requestTgtWithPassword("spock/admin@CCK.COM", "secret"); 290 | //embeddedKrbServer.getSimpleKdcServer().getKrbClient().storeTicket(tgt, new File("testtmp/tgtcc/spock.cc")); 291 | 292 | final Settings esServerSettings = Settings.builder().put(PREFIX + SettingConstants.ACCEPTOR_KEYTAB_PATH, "keytab/es_server.keytab") 293 | .put(PREFIX + SettingConstants.ACCEPTOR_PRINCIPAL, "HTTP/localhost@CCK.COM") 294 | .put(PREFIX + SettingConstants.STRIP_REALM_FROM_PRINCIPAL, true) 295 | .putArray(PREFIX + SettingConstants.ROLES+".cc_kerberos_realm_role", "spock/admin@CCK.COM") 296 | //.put(PREFIX+SettingConstants.KRB5_FILE_PATH,"") //if already set by kerby here 297 | //.put(PREFIX+SettingConstants.KRB_DEBUG, true) 298 | .build(); 299 | 300 | this.startES(esServerSettings); 301 | 302 | net.sourceforge.spnego.SpnegoHttpURLConnection hcon = new SpnegoHttpURLConnection("no.ticket.cache","1spock/admin@CCK.COM","secret"); 303 | 304 | hcon.requestCredDeleg(true); 305 | hcon.connect(new URL(getServerUri() + "/_nodes/settings")); 306 | Assert.assertEquals(200, hcon.getResponseCode()); 307 | 308 | //final CloseableHttpClient httpClient = getHttpClient(true); 309 | //final CloseableHttpResponse response = httpClient.execute(new HttpGet(new URL(getServerUri() + "/_nodes/settings").toURI())); 310 | 311 | //assertThat(response.getStatusLine().getStatusCode(), is(401)); 312 | } 313 | 314 | @Test 315 | @Ignore 316 | public void testRestBadAcceptor() throws Exception { 317 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("spock/admin@CCK.COM", "secret"); 318 | embeddedKrbServer.getSimpleKdcServer().createPrincipal("HTTP/localhost@CCK.COM", "testpwd1"); 319 | FileUtils.forceMkdir(new File("testtmp/config/keytab/")); 320 | embeddedKrbServer.getSimpleKdcServer().exportPrincipal("HTTP/localhost@CCK.COM", 321 | new File("testtmp/config/keytab/es_server.keytab")); //server, acceptor 322 | 323 | final TgtTicket tgt = embeddedKrbServer.getSimpleKdcServer().getKrbClient().requestTgtWithPassword("spock/admin@CCK.COM", "secret"); 324 | embeddedKrbServer.getSimpleKdcServer().getKrbClient().storeTicket(tgt, new File("testtmp/tgtcc/spock.cc")); 325 | 326 | final Settings esServerSettings = Settings.builder().put(PREFIX + SettingConstants.ACCEPTOR_KEYTAB_PATH, "keytab/es_server.keytab") 327 | .put(PREFIX + SettingConstants.ACCEPTOR_PRINCIPAL, "bad").put(PREFIX + SettingConstants.STRIP_REALM_FROM_PRINCIPAL, true) 328 | .putArray(PREFIX + SettingConstants.ROLES+".cc_kerberos_realm_role", "spock/admin@CCK.COM") 329 | //.put(PREFIX+SettingConstants.KRB5_FILE_PATH,"") //if already set by kerby here 330 | //.put(PREFIX+SettingConstants.KRB_DEBUG, true) 331 | .build(); 332 | 333 | this.startES(esServerSettings); 334 | 335 | net.sourceforge.spnego.SpnegoHttpURLConnection hcon = new SpnegoHttpURLConnection("com.sun.security.jgss.krb5.initiate"); 336 | 337 | hcon.requestCredDeleg(true); 338 | hcon.connect(new URL(getServerUri() + "/_nodes/settings")); 339 | Assert.assertEquals(401, hcon.getResponseCode()); 340 | 341 | //final CloseableHttpClient httpClient = getHttpClient(true); 342 | //final CloseableHttpResponse response = httpClient.execute(new HttpGet(new URL(getServerUri() + "/_nodes/settings").toURI())); 343 | 344 | //assertThat(response.getStatusLine().getStatusCode(), is(401)); 345 | } 346 | } 347 | --------------------------------------------------------------------------------