/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.accumulo.harness;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.accumulo.cluster.ClusterUser;
import org.apache.hadoop.minikdc.MiniKdc;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Creates a {@link MiniKdc} for tests to use to exercise secure Accumulo
*/
public class TestingKdc {
private static final Logger log = LoggerFactory.getLogger(TestingKdc.class);
public static final int NUM_USERS = 10;
public static final long MAX_TICKET_LIFETIME_MILLIS = 86400000; // one day
protected MiniKdc kdc = null;
protected ClusterUser accumuloServerUser = null, accumuloAdmin = null;
protected List<ClusterUser> clientPrincipals = null;
public final String ORG_NAME = "EXAMPLE", ORG_DOMAIN = "COM";
private String hostname;
private File keytabDir;
private boolean started = false;
public TestingKdc() throws Exception {
this(computeKdcDir(), computeKeytabDir(), MAX_TICKET_LIFETIME_MILLIS);
}
public static File computeKdcDir() {
File targetDir = new File(System.getProperty("user.dir"), "target");
if (!targetDir.exists())
Assert.assertTrue(targetDir.mkdirs());
Assert.assertTrue("Could not find Maven target directory: " + targetDir, targetDir.exists() && targetDir.isDirectory());
// Create the directories: target/kerberos/minikdc
File kdcDir = new File(new File(targetDir, "kerberos"), "minikdc");
assertTrue(kdcDir.mkdirs() || kdcDir.isDirectory());
return kdcDir;
}
public static File computeKeytabDir() {
File targetDir = new File(System.getProperty("user.dir"), "target");
Assert.assertTrue("Could not find Maven target directory: " + targetDir, targetDir.exists() && targetDir.isDirectory());
// Create the directories: target/kerberos/keytabs
File keytabDir = new File(new File(targetDir, "kerberos"), "keytabs");
assertTrue(keytabDir.mkdirs() || keytabDir.isDirectory());
return keytabDir;
}
public TestingKdc(File kdcDir, File keytabDir) throws Exception {
this(kdcDir, keytabDir, MAX_TICKET_LIFETIME_MILLIS);
}
public TestingKdc(File kdcDir, File keytabDir, long maxTicketLifetime) throws Exception {
requireNonNull(kdcDir, "KDC directory was null");
requireNonNull(keytabDir, "Keytab directory was null");
checkArgument(maxTicketLifetime > 0, "Ticket lifetime must be positive");
this.keytabDir = keytabDir;
this.hostname = InetAddress.getLocalHost().getCanonicalHostName();
log.debug("Starting MiniKdc in {} with keytabs in {}", kdcDir, keytabDir);
Properties kdcConf = MiniKdc.createConf();
kdcConf.setProperty(MiniKdc.ORG_NAME, ORG_NAME);
kdcConf.setProperty(MiniKdc.ORG_DOMAIN, ORG_DOMAIN);
kdcConf.setProperty(MiniKdc.MAX_TICKET_LIFETIME, Long.toString(maxTicketLifetime));
// kdcConf.setProperty(MiniKdc.DEBUG, "true");
kdc = new MiniKdc(kdcConf, kdcDir);
}
/**
* Starts the KDC and creates the principals and their keytabs
*/
public synchronized void start() throws Exception {
checkArgument(!started, "KDC was already started");
kdc.start();
Thread.sleep(1000);
// Create the identity for accumulo servers
File accumuloKeytab = new File(keytabDir, "accumulo.keytab");
String accumuloPrincipal = String.format("accumulo/%s", hostname);
log.info("Creating Kerberos principal {} with keytab {}", accumuloPrincipal, accumuloKeytab);
kdc.createPrincipal(accumuloKeytab, accumuloPrincipal);
accumuloServerUser = new ClusterUser(qualifyUser(accumuloPrincipal), accumuloKeytab);
// Create the identity for the "root" user
String rootPrincipal = "root";
File rootKeytab = new File(keytabDir, rootPrincipal + ".keytab");
log.info("Creating Kerberos principal {} with keytab {}", rootPrincipal, rootKeytab);
kdc.createPrincipal(rootKeytab, rootPrincipal);
accumuloAdmin = new ClusterUser(qualifyUser(rootPrincipal), rootKeytab);
clientPrincipals = new ArrayList<>(NUM_USERS);
// Create a number of unprivileged users for tests to use
for (int i = 1; i <= NUM_USERS; i++) {
String clientPrincipal = "client" + i;
File clientKeytab = new File(keytabDir, clientPrincipal + ".keytab");
log.info("Creating Kerberos principal {} with keytab {}", clientPrincipal, clientKeytab);
kdc.createPrincipal(clientKeytab, clientPrincipal);
clientPrincipals.add(new ClusterUser(qualifyUser(clientPrincipal), clientKeytab));
}
started = true;
}
public synchronized void stop() throws Exception {
checkArgument(started, "KDC is not started");
kdc.stop();
started = false;
}
/**
* A directory where the automatically-created keytab files are written
*/
public File getKeytabDir() {
return keytabDir;
}
/**
* A {@link ClusterUser} for Accumulo server processes to use
*/
public ClusterUser getAccumuloServerUser() {
checkArgument(started, "The KDC is not started");
return accumuloServerUser;
}
/**
* A {@link ClusterUser} which is the Accumulo "root" user
*/
public ClusterUser getRootUser() {
checkArgument(started, "The KDC is not started");
return accumuloAdmin;
}
/**
* The {@link ClusterUser} corresponding to the given offset. Represents an unprivileged user.
*
* @param offset
* The offset to fetch credentials for, valid through {@link #NUM_USERS}
*/
public ClusterUser getClientPrincipal(int offset) {
checkArgument(started, "Client principal is not initialized, is the KDC started?");
checkArgument(offset >= 0 && offset < NUM_USERS, "Offset is invalid, must be non-negative and less than " + NUM_USERS);
return clientPrincipals.get(offset);
}
/**
* @see MiniKdc#createPrincipal(File, String...)
*/
public void createPrincipal(File keytabFile, String... principals) throws Exception {
checkArgument(started, "KDC is not started");
kdc.createPrincipal(keytabFile, principals);
}
/**
* @return the name for the realm
*/
public String getOrgName() {
return ORG_NAME;
}
/**
* @return the domain for the realm
*/
public String getOrgDomain() {
return ORG_DOMAIN;
}
/**
* Qualify a username (only the primary from the kerberos principal) with the proper realm
*
* @param primary
* The primary or primary and instance
*/
public String qualifyUser(String primary) {
return String.format("%s@%s.%s", primary, getOrgName(), getOrgDomain());
}
}