/*
* Copyright (c) 2014 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.security.keystore.resource;
import java.net.URI;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.SecurityContext;
import com.emc.storageos.security.keystore.impl.*;
import com.emc.vipr.model.keystore.*;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import com.emc.storageos.coordinator.client.service.CoordinatorClient;
import com.emc.storageos.security.audit.AuditLogManager;
import com.emc.storageos.security.authentication.StorageOSUser;
import com.emc.storageos.security.authorization.CheckPermission;
import com.emc.storageos.security.authorization.Role;
import com.emc.storageos.security.exceptions.SecurityException;
import com.emc.storageos.security.keystore.impl.CoordinatorConfigStoringHelper;
import com.emc.storageos.security.keystore.impl.KeyCertificatePairGenerator;
import com.emc.storageos.security.keystore.impl.KeyStoreUtil;
import com.emc.storageos.services.OperationTypeEnum;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
/**
* A resource for truststore related requests
*/
@Path("/vdc/truststore")
public class TrustStoreResource {
private static final String EVENT_SERVICE_TYPE = "Truststore";
private static Logger log = LoggerFactory.getLogger(TrustStoreResource.class);
@Context
protected SecurityContext sc;
@Autowired
protected AuditLogManager auditMgr;
private CoordinatorClient coordinator;
private CoordinatorConfigStoringHelper coordConfigStoringHelper;
private KeyStore viprKeyStore;
protected CertificateVersionHelper certificateVersionHelper;
public void setCertificateVersionHelper(
CertificateVersionHelper certificateVersionHelper) {
this.certificateVersionHelper = certificateVersionHelper;
}
public void setCoordinator(CoordinatorClient coordinator) {
this.coordinator = coordinator;
}
public void setCoordConfigStoringHelper(CoordinatorConfigStoringHelper coordConfigStoringHelper) {
this.coordConfigStoringHelper = coordConfigStoringHelper;
}
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN }, blockProxies = true)
public TrustedCertificates getTrustedCertificates() {
List<TrustedCertificate> trustedCertsList = new ArrayList<TrustedCertificate>();
try {
for (String alias : Collections.list(getKeyStore().aliases())) {
log.debug("get alias {}", alias);
if (getKeyStore().isCertificateEntry(alias)) {
boolean userSupplied = KeystoreEngine.isUserSuppliedCerts(alias);
Certificate cert = getKeyStore().getCertificate(alias);
TrustedCertificate tc = new TrustedCertificate(
KeyCertificatePairGenerator.getCertificateAsString(cert), userSupplied);
trustedCertsList.add(tc);
}
}
} catch (KeyStoreException e) {
log.error(e.getMessage(), e);
throw new IllegalStateException(e);
} catch (CertificateEncodingException e) {
log.error(e.getMessage(), e);
throw SecurityException.fatals.couldNotParseCertificateToString(e);
}
TrustedCertificates certs = new TrustedCertificates();
certs.setTrustedCertificates(trustedCertsList);
return certs;
}
@PUT
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN }, blockProxies = true)
public TrustedCertificates addOrRemoveTrustedCertificate(
TrustedCertificateChanges changes) {
// to hold all kinds of certs in request to support add/remove in batch, and to support partial success as well
class UpdateResult {
int nAdded = 0;
int nRemoved = 0;
List<Integer> failToParse = new ArrayList<>();
List<Integer> expired = new ArrayList<>();
List<Integer> notExisted = new ArrayList<>();
public boolean hasAnyFailure() {
return ( failToParse.isEmpty() && expired.isEmpty() && notExisted.isEmpty() ) ? false:true;
}
public boolean hasSuccess() {
return ( nAdded == 0 && nRemoved == 0 ) ? false:true;
}
}
if (!coordinator.isClusterUpgradable()) {
throw SecurityException.retryables.updatingKeystoreWhileClusterIsUnstable();
}
KeyStore keystore = getKeyStore();
UpdateResult result = new UpdateResult();
List<String> certsToAdd = changes.getAdd();
if (certsToAdd != null) {
for (int i = 0; i < certsToAdd.size(); i++) {
String certString = certsToAdd.get(i);
try {
Certificate cert = KeyCertificatePairGenerator.getCertificateFromString(certString);
// if we were able to parse the cert, and there wasn't more that 1
// cert in this certificate entry
if (cert != null
&& StringUtils.countMatches(certString, KeyCertificatePairGenerator.PEM_BEGIN_CERT) == 1) {
String alias = DigestUtils.sha512Hex(cert.getEncoded());
if (!keystore.containsAlias(alias)) {
X509Certificate x509cert = (X509Certificate) cert;
Date now = new Date();
if (x509cert.getNotAfter().before(now)) {
log.warn("The following certificate has expired: {}", certString);
result.expired.add(i+1); // start from 1 for easier understanding for user
} else if (now.before(x509cert.getNotBefore())) {
log.warn("The following certificate is not yet valid: {} ", certString);
result.expired.add(i+1);
} else { // good one
keystore.setCertificateEntry(alias, cert);
result.nAdded ++;
}
}
} else {
result.failToParse.add(i+1);
}
} catch (KeyStoreException e) {
throw new IllegalStateException("keystore is not initialized", e);
} catch (CertificateException e) {
log.debug(e.getMessage(), e);
result.failToParse.add(i+1);
}
}
}
List<String> certsToRemove = changes.getRemove();
if (certsToRemove != null) {
for (int i = 0; i < certsToRemove.size(); i++) {
String certString = certsToRemove.get(i);
Certificate cert;
try {
cert = KeyCertificatePairGenerator.getCertificateFromString(certString);
if (cert != null
&& StringUtils.countMatches(certString, KeyCertificatePairGenerator.PEM_BEGIN_CERT) == 1) {
keystore.deleteEntry(DigestUtils.sha512Hex(cert.getEncoded()));
result.nRemoved ++;
} else {
result.failToParse.add(i+1);
}
} catch (CertificateException | KeyStoreException e) {
log.warn("the following certificate could not be deleted: {}", certString, e);
result.notExisted.add(i+1);
}
}
}
// set AcceptAll to No if any certificate is added.
if ( (result.nAdded > 0) && getTruststoreSettings().isAcceptAllCertificates()) {
TruststoreSettingsChanges settingsChanges = new TruststoreSettingsChanges();
settingsChanges.setAcceptAllCertificates(false);
changeSettingInternal(settingsChanges);
}
if (result.hasSuccess()) {
// To update the zk and then make service get notified on change.
recordTrustChangeIfAny();
auditTruststore(OperationTypeEnum.UPDATE_TRUSTED_CERTIFICATES, changes);
}
if (result.hasAnyFailure()) {
int nAdd = (certsToAdd == null) ? 0 : certsToAdd.size();
int nRemove = (certsToRemove == null) ? 0 : certsToRemove.size();
int nFailToAdd = result.failToParse.size() + result.expired.size();
int nFailToRemove = result.notExisted.size();
throw APIException.badRequests.trustStoreUpdatePartialSuccess(nAdd, nFailToAdd, result.failToParse, result.expired,
nRemove, nFailToRemove, result.notExisted);
}
// All good
return getTrustedCertificates();
}
/**
* set a flag in zk if any change happened.
*/
private void recordTrustChangeIfAny() {
try {
coordConfigStoringHelper.createOrUpdateConfig(System.nanoTime(),
DistributedKeyStoreImpl.TRUSTED_CERTIFICATES_LOCK,
DistributedKeyStoreImpl.TRUSTED_CERTIFICATES_CONFIG_KIND,
DistributedKeyStoreImpl.UPDATE_LOG,
DistributedKeyStoreImpl.UPDATE_TIME);
} catch (Exception e) {
throw SecurityException.fatals.failToNotifyChange(e);
}
}
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN }, blockProxies = true)
@Path("/settings")
public TruststoreSettings getTruststoreSettings() {
TruststoreSettings settings = new TruststoreSettings();
settings.setAcceptAllCertificates(KeyStoreUtil.getAcceptAllCerts(coordConfigStoringHelper));
return settings;
}
@PUT
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN }, blockProxies = true)
@Path("/settings")
public TruststoreSettings updateTruststoreSettings(TruststoreSettingsChanges changes) {
changeSettingInternal(changes);
/*
* todo temp comments out
* if (!certificateVersionHelper.updateCertificateVersion()) {
* throw SecurityException.fatals.failedRebootAfterKeystoreChange();
* }
*/
return getTruststoreSettings();
}
/**
* @param changes
* @return
*/
private void changeSettingInternal(TruststoreSettingsChanges changes) {
if (!coordinator.isClusterUpgradable()) {
throw SecurityException.retryables.updatingKeystoreWhileClusterIsUnstable();
}
TruststoreSettings currentSettings = getTruststoreSettings();
// if current and changed settings are different
if ((changes.getAcceptAllCertificates() != null) &&
(currentSettings.isAcceptAllCertificates() ^ changes.getAcceptAllCertificates().booleanValue())) {
try {
KeyStoreUtil.setAcceptAllCertificates(coordConfigStoringHelper,
changes.getAcceptAllCertificates());
} catch (Exception e) {
throw SecurityException.fatals.failedToSetTruststoreSettings(e);
}
auditTruststore(OperationTypeEnum.UPDATE_TRUSTSTORE_SETTINGS, changes);
} else {
throw APIException.badRequests
.mustHaveAtLeastOneChange(TrustedCertificateChanges.class.toString());
}
}
/**
* Get StorageOSUser from the security context
*
* @return
*/
private StorageOSUser getUserFromContext() {
if (!hasValidUserInContext()) {
throw APIException.forbidden.invalidSecurityContext();
}
return (StorageOSUser) sc.getUserPrincipal();
}
/**
* Determine if the security context has a valid StorageOSUser object
*
* @return true if the StorageOSUser is present
*/
private boolean hasValidUserInContext() {
if ((sc != null) && (sc.getUserPrincipal() instanceof StorageOSUser)) {
return true;
} else {
return false;
}
}
/**
* Record audit log for Truststore service
*
* @param auditType
* Type of AuditLog
* @param descparams
* Description paramters
*/
private void auditTruststore(OperationTypeEnum auditType, Object... descparams) {
URI username = URI.create(getUserFromContext().getName());
auditMgr.recordAuditLog(null, username, EVENT_SERVICE_TYPE, auditType,
System.currentTimeMillis(), AuditLogManager.AUDITLOG_SUCCESS, null,
descparams);
}
/**
* Record audit log for Truststore service
*
* @param auditType
* Type of AuditLog
* @param descparams
* Description paramters
*/
private void auditTruststorePatialSuccess(OperationTypeEnum auditType,
Object... descparams) {
URI username = URI.create(getUserFromContext().getName());
auditMgr.recordAuditLog(null, username, EVENT_SERVICE_TYPE, auditType,
System.currentTimeMillis(), "PARTIAL_SUCCESS", null, descparams);
}
private KeyStore getKeyStore() {
if (viprKeyStore == null) {
try {
viprKeyStore = KeyStoreUtil.getViPRKeystore(coordinator);
} catch (Exception e) {
log.error("Failed to load the VIPR keystore", e);
throw new IllegalStateException(e);
}
}
return viprKeyStore;
}
}