/*
* 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.ambari.server.security.encryption;
import java.security.KeyStore;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.security.credential.Credential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
/**
* InMemoryCredentialStore is a CredentialStore implementation that creates and manages
* a JCEKS (Java Cryptography Extension KeyStore) in memory. The key store and its contents are
* encrypted using the key from the supplied {@link MasterKeyService}.
* <p/>
* This class handles the details of the in-memory storage buffer and associated input and output
* streams. Each credential is stored in its own KeyStore that may be be purged upon some
* retention timeout - if specified.
*/
public class InMemoryCredentialStore extends AbstractCredentialStore {
private static final Logger LOG = LoggerFactory.getLogger(InMemoryCredentialStore.class);
/**
* A cache containing the KeyStore data
*/
private final Cache<String, KeyStore> cache;
/**
* Constructs a new InMemoryCredentialStore where credentials have no retention timeout
*/
public InMemoryCredentialStore() {
this(0, TimeUnit.MINUTES, false);
}
/**
* Constructs a new InMemoryCredentialStore with a specified credential timeout
*
* @param retentionDuration the time in some units to keep stored credentials, from the time they are added
* @param units the units for the retention duration (minutes, seconds, etc...)
* @param activelyPurge true to actively purge credentials after the retention time has expired;
* otherwise false, to passively purge credentials after the retention time has expired
*/
public InMemoryCredentialStore(final long retentionDuration, final TimeUnit units, boolean activelyPurge) {
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
// If the retentionDuration is less the 1, then no retention policy is to be enforced
if (retentionDuration > 0) {
// If actively purging expired credentials, set up a timer to periodically clean the cache
if (activelyPurge) {
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
Thread t = Executors.defaultThreadFactory().newThread(runnable);
if (t != null) {
t.setName(String.format("%s active cleanup timer", InMemoryCredentialStore.class.getSimpleName()));
t.setDaemon(true);
}
return t;
}
};
Runnable runnable = new Runnable() {
@Override
public void run() {
if (LOG.isDebugEnabled()) {
LOG.debug("Cleaning up cache due to retention timeout of {} milliseconds",
units.toMillis(retentionDuration));
}
cache.cleanUp();
}
};
Executors.newSingleThreadScheduledExecutor(threadFactory).schedule(runnable, 1, TimeUnit.MINUTES);
}
builder.expireAfterWrite(retentionDuration, units);
}
cache = builder.build();
}
/**
* Adds a new credential to this CredentialStore
* <p/>
* The supplied key will be converted into UTF-8 bytes before being stored.
* <p/>
* This implementation is thread-safe, allowing one thread at a time to access the credential store.
*
* @param alias a string declaring the alias (or name) of the credential
* @param credential the credential to store
* @throws AmbariException if an error occurs while storing the new credential
*/
@Override
public void addCredential(String alias, Credential credential) throws AmbariException {
if ((alias == null) || alias.isEmpty()) {
throw new IllegalArgumentException("Alias cannot be null or empty.");
}
Lock lock = getLock();
lock.lock();
try {
KeyStore keyStore = loadKeyStore(null, DEFAULT_STORE_TYPE);
addCredential(keyStore, alias, credential);
cache.put(alias, keyStore);
} finally {
lock.unlock();
}
}
/**
* Retrieves the specified credential from this CredentialStore
* <p/>
* This implementation is thread-safe, allowing one thread at a time to access the credential store.
*
* @param alias a string declaring the alias (or name) of the credential
* @return a Credential or null of not found
* @throws AmbariException if an error occurs while retrieving the new credential
*/
@Override
public Credential getCredential(String alias) throws AmbariException {
Credential credential = null;
if ((alias != null) && !alias.isEmpty()) {
Lock lock = getLock();
lock.lock();
try {
KeyStore keyStore = cache.getIfPresent(alias);
if (keyStore != null) {
credential = getCredential(keyStore, alias);
}
} finally {
lock.unlock();
}
}
return credential;
}
/**
* Removes the specified credential from this CredentialStore
* <p/>
* This implementation is thread-safe, allowing one thread at a time to access the credential store.
*
* @param alias a string declaring the alias (or name) of the credential
* @throws AmbariException if an error occurs while removing the new credential
*/
@Override
public void removeCredential(String alias) throws AmbariException {
if (alias != null) {
Lock lock = getLock();
lock.lock();
try {
cache.invalidate(alias);
} finally {
lock.unlock();
}
}
}
/**
* Returns a list of the alias names for the credentials stored in the CredentialStore
* <p/>
* This implementation is thread-safe, allowing one thread at a time to access the credential store.
*
* @return a Set of Strings representing alias names for the credentials stored in the CredentialStore
* @throws AmbariException if an error occurs while searching forthe credential
*/
@Override
public Set<String> listCredentials() throws AmbariException {
Lock lock = getLock();
lock.lock();
try {
return new HashSet<>(cache.asMap().keySet());
} finally {
lock.unlock();
}
}
/**
* Tests this CredentialStore for the existence of a credential with the specified alias
* <p/>
* This implementation is thread-safe, allowing one thread at a time to access the credential store.
*
* @param alias a string declaring the alias (or name) of the credential
* @return true if the alias exists; otherwise false
* @throws AmbariException if an error occurs while searching forthe credential
*/
@Override
public boolean containsCredential(String alias) throws AmbariException {
Lock lock = getLock();
lock.lock();
try {
return (alias != null) && !alias.isEmpty() && (cache.getIfPresent(alias) != null);
} finally {
lock.unlock();
}
}
@Override
protected void persistCredentialStore(KeyStore keyStore) throws AmbariException {
throw new UnsupportedOperationException();
}
@Override
protected KeyStore loadCredentialStore() throws AmbariException {
throw new UnsupportedOperationException();
}
}