content: // 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.cloudstack.ca;
  
  import java.io.IOException;
  import java.math.BigInteger;
  import java.security.GeneralSecurityException;
  import java.security.KeyStore;
  import java.security.KeyStoreException;
  import java.security.cert.CertificateExpiredException;
  import java.security.cert.CertificateNotYetValidException;
  import java.security.cert.CertificateParsingException;
  import java.security.cert.X509Certificate;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.ConcurrentHashMap;
  
  import javax.inject.Inject;
  import javax.naming.ConfigurationException;
  import javax.net.ssl.SSLContext;
  import javax.net.ssl.SSLEngine;
  
  import org.apache.cloudstack.api.ApiErrorCode;
  import org.apache.cloudstack.api.ServerApiException;
  import org.apache.cloudstack.api.command.admin.ca.IssueCertificateCmd;
  import org.apache.cloudstack.api.command.admin.ca.ListCAProvidersCmd;
  import org.apache.cloudstack.api.command.admin.ca.ListCaCertificateCmd;
  import org.apache.cloudstack.api.command.admin.ca.ProvisionCertificateCmd;
  import org.apache.cloudstack.api.command.admin.ca.RevokeCertificateCmd;
  import org.apache.cloudstack.context.CallContext;
  import org.apache.cloudstack.framework.ca.CAProvider;
  import org.apache.cloudstack.framework.ca.Certificate;
  import org.apache.cloudstack.framework.config.ConfigKey;
  import org.apache.cloudstack.managed.context.ManagedContextRunnable;
  import org.apache.cloudstack.poll.BackgroundPollManager;
  import org.apache.cloudstack.poll.BackgroundPollTask;
  import org.apache.cloudstack.utils.identity.ManagementServerNode;
  import org.apache.cloudstack.utils.security.CertUtils;
  import org.joda.time.DateTime;
  import org.joda.time.DateTimeZone;
  
  import com.cloud.agent.AgentManager;
  import com.cloud.alert.AlertManager;
  import com.cloud.certificate.CrlVO;
  import com.cloud.certificate.dao.CrlDao;
  import com.cloud.event.ActionEvent;
  import com.cloud.event.EventTypes;
  import com.cloud.exception.AgentUnavailableException;
  import com.cloud.exception.OperationTimedoutException;
  import com.cloud.host.Host;
  import com.cloud.host.Status;
  import com.cloud.host.dao.HostDao;
  import com.cloud.utils.component.ManagerBase;
  import com.cloud.utils.exception.CloudRuntimeException;
  import org.apache.commons.lang3.StringUtils;
  
  public class CAManagerImpl extends ManagerBase implements CAManager {
  
      @Inject
      private CrlDao crlDao;
      @Inject
      private HostDao hostDao;
      @Inject
      private AgentManager agentManager;
      @Inject
      private BackgroundPollManager backgroundPollManager;
      @Inject
      private AlertManager alertManager;
  
      private static CAProvider configuredCaProvider;
      private static Map<String, CAProvider> caProviderMap = new HashMap<>();
      private static Map<String, Date> alertMap = new ConcurrentHashMap<>();
      private static Map<String, X509Certificate> activeCertMap = new ConcurrentHashMap<>();
  
      private List<CAProvider> caProviders;
  
      private CAProvider getConfiguredCaProvider() {
          if (configuredCaProvider != null) {
              return configuredCaProvider;
          }
          if (caProviderMap.containsKey(CAProviderPlugin.value()) && caProviderMap.get(CAProviderPlugin.value()) != null) {
              configuredCaProvider = caProviderMap.get(CAProviderPlugin.value());
              return configuredCaProvider;
          }
          throw new CloudRuntimeException("Failed to find default configured CA provider plugin");
      }
  
      private CAProvider getCAProvider(final String provider) {
          if (StringUtils.isEmpty(provider)) {
              return getConfiguredCaProvider();
          }
          final String caProviderName = provider.toLowerCase();
          if (!caProviderMap.containsKey(caProviderName)) {
              throw new CloudRuntimeException(String.format("CA provider plugin '%s' not found", caProviderName));
          }
          final CAProvider caProvider = caProviderMap.get(caProviderName);
          if (caProvider == null) {
              throw new CloudRuntimeException(String.format("CA provider plugin '%s' returned is null", caProviderName));
          }
          return caProvider;
      }
  
      ///////////////////////////////////////////////////////////
      /////////////// CA Manager API Handlers ///////////////////
      ///////////////////////////////////////////////////////////
  
      @Override
      public List<CAProvider> getCaProviders() {
          return caProviders;
      }
  
      @Override
      public Map<String, X509Certificate> getActiveCertificatesMap() {
          return activeCertMap;
      }
  
      @Override
      public boolean canProvisionCertificates() {
          return getConfiguredCaProvider().canProvisionCertificates();
      }
  
      @Override
      public String getCaCertificate(final String caProvider) throws IOException {
          final CAProvider provider = getCAProvider(caProvider);
          return CertUtils.x509CertificatesToPem(provider.getCaCertificate());
      }
  
      @Override
      @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_ISSUE, eventDescription = "issuing certificate", async = true)
      public Certificate issueCertificate(final String csr, final List<String> domainNames, final List<String> ipAddresses, final Integer validityDuration, final String caProvider) {
          CallContext.current().setEventDetails("domain(s): " + domainNames + " addresses: " + ipAddresses);
          final CAProvider provider = getCAProvider(caProvider);
          Integer validity = CAManager.CertValidityPeriod.value();
          if (validityDuration != null) {
              validity = validityDuration;
          }
          if (StringUtils.isEmpty(csr)) {
              if (domainNames == null || domainNames.isEmpty()) {
                  throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "No domains or CSR provided");
              }
              return provider.issueCertificate(domainNames, ipAddresses, validity);
          }
          return provider.issueCertificate(csr, domainNames, ipAddresses, validity);
      }
  
      @Override
      @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_REVOKE, eventDescription = "revoking certificate", async = true)
      public boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String caProvider) {
          CallContext.current().setEventDetails("cert serial: " + certSerial);
          final CrlVO crl = crlDao.revokeCertificate(certSerial, certCn);
          if (crl != null && crl.getCertSerial().equals(certSerial)) {
              final CAProvider provider = getCAProvider(caProvider);
              return provider.revokeCertificate(certSerial, certCn);
          }
          return false;
      }
  
      @Override
      @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_PROVISION, eventDescription = "provisioning certificate for host", async = true)
      public boolean provisionCertificate(final Host host, final Boolean reconnect, final String caProvider) {
          if (host == null) {
              throw new CloudRuntimeException("Unable to find valid host to renew certificate for");
          }
          CallContext.current().setEventDetails("host id: " + host.getId());
          CallContext.current().putContextParameter(Host.class, host.getUuid());
          String csr = null;
  
          try {
              if (host.getType() != Host.Type.ConsoleProxy && host.getType() != Host.Type.SecondaryStorageVM) {
                  csr = generateKeyStoreAndCsr(host, null);
                  if (StringUtils.isEmpty(csr)) {
                      return false;
                  }
              }
              final Certificate certificate = issueCertificate(csr, Arrays.asList(host.getName(), host.getPrivateIpAddress()), Arrays.asList(host.getPrivateIpAddress(), host.getPublicIpAddress(), host.getStorageIpAddress()), CAManager.CertValidityPeriod.value(), caProvider);
              return deployCertificate(host, certificate, reconnect, null);
          } catch (final AgentUnavailableException | OperationTimedoutException e) {
              logger.error("Host/agent is not available or operation timed out, failed to setup keystore and generate CSR for host/agent {}, due to: ", host, e);
              throw new CloudRuntimeException(String.format("Failed to generate keystore and get CSR from the host/agent %s", host));
          }
      }
  
      @Override
      public String generateKeyStoreAndCsr(final Host host, final Map<String, String> sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException {
          final SetupKeyStoreCommand cmd = new SetupKeyStoreCommand(CertValidityPeriod.value());
          if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) {
              cmd.setAccessDetail(sshAccessDetails);
          }
          CallContext.current().setEventDetails("generating keystore and CSR for host id: " + host.getId());
          final SetupKeystoreAnswer answer = (SetupKeystoreAnswer)agentManager.send(host.getId(), cmd);
          return answer.getCsr();
      }
  
      private boolean isValidSystemVMType(Host.Type type) {
          return Host.Type.SecondaryStorageVM.equals(type) ||
                  Host.Type.ConsoleProxy.equals(type);
      }
  
      @Override
      public boolean deployCertificate(final Host host, final Certificate certificate, final Boolean reconnect, final Map<String, String> sshAccessDetails)
              throws AgentUnavailableException, OperationTimedoutException {
          final SetupCertificateCommand cmd = new SetupCertificateCommand(certificate);
          if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) {
              cmd.setAccessDetail(sshAccessDetails);
          }
          CallContext.current().setEventDetails("deploying certificate for host id: " + host.getId());
          final SetupCertificateAnswer answer = (SetupCertificateAnswer)agentManager.send(host.getId(), cmd);
          if (answer.getResult()) {
              CallContext.current().setEventDetails("successfully deployed certificate for host id: " + host.getId());
          } else {
              CallContext.current().setEventDetails("failed to deploy certificate for host id: " + host.getId());
          }
  
          if (answer.getResult()) {
              getActiveCertificatesMap().put(host.getPrivateIpAddress(), certificate.getClientCertificate());
              if (sshAccessDetails == null && reconnect != null && reconnect) {
                  logger.info("Successfully setup certificate on host, reconnecting with agent [{}] with address={}", host, host.getPublicIpAddress());
                  try {
                      agentManager.reconnect(host.getId());
                  } catch (AgentUnavailableException | CloudRuntimeException e) {
                      logger.debug("Error when reconnecting to host: {}", host, e);
                  }
              }
              return true;
          }
          return false;
      }
  
      @Override
      public void purgeHostCertificate(final Host host) {
          if (host == null) {
              return;
          }
          final String privateAddress = host.getPrivateIpAddress();
          final String publicAddress = host.getPublicIpAddress();
          final Map<String, X509Certificate> activeCertsMap = getActiveCertificatesMap();
          if (StringUtils.isNotEmpty(privateAddress) && activeCertsMap.containsKey(privateAddress)) {
              activeCertsMap.remove(privateAddress);
          }
          if (StringUtils.isNotEmpty(publicAddress) && activeCertsMap.containsKey(publicAddress)) {
              activeCertsMap.remove(publicAddress);
          }
      }
  
      @Override
      public void sendAlert(final Host host, final String subject, final String message) {
          if (host == null) {
              return;
          }
          alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_CA_CERT, host.getDataCenterId(), host.getPodId(), subject, message);
      }
  
      @Override
      public SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress) throws GeneralSecurityException, IOException {
          if (sslContext == null) {
              throw new CloudRuntimeException("SSLContext provided to create SSLEngine is null, aborting");
          }
          if (StringUtils.isEmpty(remoteAddress)) {
              throw new CloudRuntimeException("Remote client address connecting to mgmt server cannot be empty/null");
          }
          return getConfiguredCaProvider().createSSLEngine(sslContext, remoteAddress, getActiveCertificatesMap());
      }
  
      @Override
      public KeyStore getManagementKeyStore() throws KeyStoreException {
          return getConfiguredCaProvider().getManagementKeyStore();
      }
  
      @Override
      public char[] getKeyStorePassphrase() {
          return getConfiguredCaProvider().getKeyStorePassphrase();
      }
  
      ////////////////////////////////////////////////////
      /////////////// CA Manager Setup ///////////////////
      ////////////////////////////////////////////////////
  
      public static final class CABackgroundTask extends ManagedContextRunnable implements BackgroundPollTask {
          private CAManager caManager;
          private HostDao hostDao;
  
          public CABackgroundTask(final CAManager caManager, final HostDao hostDao) {
              this.caManager = caManager;
              this.hostDao = hostDao;
          }
  
          @Override
          protected void runInContext() {
              try {
                  if (logger.isTraceEnabled()) {
                      logger.trace("CA background task is running...");
                  }
                  final DateTime now = DateTime.now(DateTimeZone.UTC);
                  final Map<String, X509Certificate> certsMap = caManager.getActiveCertificatesMap();
                  for (final Iterator<Map.Entry<String, X509Certificate>> it = certsMap.entrySet().iterator(); it.hasNext();) {
                      final Map.Entry<String, X509Certificate> entry = it.next();
                      if (entry == null) {
                          continue;
                      }
                      final String hostIp = entry.getKey();
                      final X509Certificate certificate = entry.getValue();
                      if (certificate == null) {
                          it.remove();
                          continue;
                      }
                      final Host host = hostDao.findByIp(hostIp);
                      if (host == null || host.getManagementServerId() == null || host.getManagementServerId() != ManagementServerNode.getManagementServerId() || host.getStatus() != Status.Up) {
                          if (host == null || (host.getManagementServerId() != null && host.getManagementServerId() != ManagementServerNode.getManagementServerId())) {
                              it.remove();
                          }
                          continue;
                      }
  
                      final String hostDescription = String.format("host id=%d, uuid=%s, name=%s, ip=%s, zone id=%d", host.getId(), host.getUuid(), host.getName(), hostIp, host.getDataCenterId());
  
                      try {
                          certificate.checkValidity(now.plusDays(CertExpiryAlertPeriod.valueIn(host.getClusterId())).toDate());
                      } catch (final CertificateExpiredException | CertificateNotYetValidException e) {
                          logger.warn("Certificate is going to expire for " + hostDescription, e);
                          if (AutomaticCertRenewal.valueIn(host.getClusterId())) {
                              try {
                                  logger.debug("Attempting certificate auto-renewal for " + hostDescription, e);
                                  boolean result = caManager.provisionCertificate(host, false, null);
                                  if (result) {
                                      logger.debug("Succeeded in auto-renewing certificate for " + hostDescription, e);
                                  } else {
                                      logger.debug("Failed in auto-renewing certificate for " + hostDescription, e);
                                  }
                              } catch (final Throwable ex) {
                                  logger.warn("Failed to auto-renew certificate for " + hostDescription + ", with error=", ex);
                                  caManager.sendAlert(host, "Certificate auto-renewal failed for " + hostDescription,
                                          String.format("Certificate is going to expire for %s. Auto-renewal failed to renew the certificate, please renew it manually. It is not valid after %s.",
                                                  hostDescription, certificate.getNotAfter()));
                              }
                          } else {
                              if (alertMap.containsKey(hostIp)) {
                                  final Date lastSentDate = alertMap.get(hostIp);
                                  if (now.minusDays(1).toDate().before(lastSentDate)) {
                                      continue;
                                  }
                              }
                              caManager.sendAlert(host, "Certificate expiring soon for " + hostDescription,
                                      String.format("Certificate is going to expire for %s. Please renew it, it is not valid after %s.", hostDescription, certificate.getNotAfter()));
                              alertMap.put(hostIp, new Date());
                          }
                      }
                  }
              } catch (final Throwable t) {
                  logger.error("Error trying to run CA background task", t);
              }
          }
  
          @Override
          public Long getDelay() {
              return CABackgroundJobDelay.value() * 1000L;
          }
      }
  
      public void setCaProviders(final List<CAProvider> caProviders) {
          this.caProviders = caProviders;
          initializeCaProviderMap();
      }
  
      private void initializeCaProviderMap() {
          if (caProviderMap != null && caProviderMap.size() != caProviders.size()) {
              for (final CAProvider caProvider : caProviders) {
                  caProviderMap.put(caProvider.getProviderName().toLowerCase(), caProvider);
              }
          }
      }
  
      @Override
      public boolean start() {
          super.start();
          initializeCaProviderMap();
          if (caProviderMap.containsKey(CAProviderPlugin.value())) {
              configuredCaProvider = caProviderMap.get(CAProviderPlugin.value());
          }
          if (configuredCaProvider == null) {
              logger.error("Failed to find valid configured CA provider, please check!");
              return false;
          }
          return true;
      }
  
      @Override
      public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
          backgroundPollManager.submitTask(new CABackgroundTask(this, hostDao));
          return true;
      }
  
      //////////////////////////////////////////////////////////
      /////////////// CA Manager Descriptors ///////////////////
      //////////////////////////////////////////////////////////
  
      @Override
      public List<Class<?>> getCommands() {
          final List<Class<?>> cmdList = new ArrayList<Class<?>>();
          cmdList.add(ListCAProvidersCmd.class);
          cmdList.add(ListCaCertificateCmd.class);
          cmdList.add(IssueCertificateCmd.class);
          cmdList.add(ProvisionCertificateCmd.class);
          cmdList.add(RevokeCertificateCmd.class);
          return cmdList;
      }
  
      @Override
      public String getConfigComponentName() {
          return CAManager.class.getSimpleName();
      }
  
      @Override
      public ConfigKey<?>[] getConfigKeys() {
          return new ConfigKey<?>[] {CAProviderPlugin, CertKeySize, CertSignatureAlgorithm, CertValidityPeriod,
                  AutomaticCertRenewal, AllowHostIPInSysVMAgentCert, CABackgroundJobDelay, CertExpiryAlertPeriod,
                  CertManagementCustomSubjectAlternativeName
          };
      }
  
      @Override
      public boolean isManagementCertificate(java.security.cert.Certificate certificate) throws CertificateParsingException {
          return getConfiguredCaProvider().isManagementCertificate(certificate);
      }
  }
  