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.api.command;
  
  import java.io.IOException;
  import java.io.StringWriter;
  import java.net.InetAddress;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  
  import javax.inject.Inject;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import javax.servlet.http.HttpSession;
  import javax.xml.parsers.DocumentBuilder;
  import javax.xml.parsers.DocumentBuilderFactory;
  import javax.xml.parsers.ParserConfigurationException;
  import javax.xml.stream.FactoryConfigurationError;
  import javax.xml.transform.Transformer;
  import javax.xml.transform.TransformerException;
  import javax.xml.transform.dom.DOMSource;
  import javax.xml.transform.stream.StreamResult;
  
  import org.apache.cloudstack.api.APICommand;
  import org.apache.cloudstack.api.ApiErrorCode;
  import org.apache.cloudstack.api.ApiServerService;
  import org.apache.cloudstack.api.BaseCmd;
  import org.apache.cloudstack.api.ServerApiException;
  import org.apache.cloudstack.api.auth.APIAuthenticationType;
  import org.apache.cloudstack.api.auth.APIAuthenticator;
  import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
  import org.apache.cloudstack.api.response.SAMLMetaDataResponse;
  import org.apache.cloudstack.saml.SAML2AuthManager;
  import org.apache.cloudstack.saml.SAMLProviderMetadata;
  import org.apache.cloudstack.utils.security.ParserUtils;
  import org.opensaml.Configuration;
  import org.opensaml.DefaultBootstrap;
  import org.opensaml.common.xml.SAMLConstants;
  import org.opensaml.saml2.core.NameIDType;
  import org.opensaml.saml2.metadata.AssertionConsumerService;
  import org.opensaml.saml2.metadata.ContactPerson;
  import org.opensaml.saml2.metadata.ContactPersonTypeEnumeration;
  import org.opensaml.saml2.metadata.EmailAddress;
  import org.opensaml.saml2.metadata.EntityDescriptor;
  import org.opensaml.saml2.metadata.GivenName;
  import org.opensaml.saml2.metadata.KeyDescriptor;
  import org.opensaml.saml2.metadata.LocalizedString;
  import org.opensaml.saml2.metadata.NameIDFormat;
  import org.opensaml.saml2.metadata.Organization;
  import org.opensaml.saml2.metadata.OrganizationName;
  import org.opensaml.saml2.metadata.OrganizationURL;
  import org.opensaml.saml2.metadata.SPSSODescriptor;
  import org.opensaml.saml2.metadata.SingleLogoutService;
  import org.opensaml.saml2.metadata.impl.AssertionConsumerServiceBuilder;
  import org.opensaml.saml2.metadata.impl.ContactPersonBuilder;
  import org.opensaml.saml2.metadata.impl.EmailAddressBuilder;
  import org.opensaml.saml2.metadata.impl.EntityDescriptorBuilder;
  import org.opensaml.saml2.metadata.impl.GivenNameBuilder;
  import org.opensaml.saml2.metadata.impl.KeyDescriptorBuilder;
  import org.opensaml.saml2.metadata.impl.NameIDFormatBuilder;
  import org.opensaml.saml2.metadata.impl.OrganizationBuilder;
  import org.opensaml.saml2.metadata.impl.OrganizationNameBuilder;
  import org.opensaml.saml2.metadata.impl.OrganizationURLBuilder;
  import org.opensaml.saml2.metadata.impl.SPSSODescriptorBuilder;
  import org.opensaml.saml2.metadata.impl.SingleLogoutServiceBuilder;
  import org.opensaml.xml.ConfigurationException;
  import org.opensaml.xml.io.Marshaller;
  import org.opensaml.xml.io.MarshallingException;
  import org.opensaml.xml.security.SecurityException;
  import org.opensaml.xml.security.credential.UsageType;
  import org.opensaml.xml.security.keyinfo.KeyInfoGenerator;
  import org.opensaml.xml.security.x509.BasicX509Credential;
  import org.opensaml.xml.security.x509.X509KeyInfoGeneratorFactory;
  import org.w3c.dom.Document;
  
  import com.cloud.api.response.ApiResponseSerializer;
  import com.cloud.user.Account;
  import com.cloud.utils.HttpUtils;
  
  @APICommand(name = "getSPMetadata", description = "Returns SAML2 CloudStack Service Provider MetaData", responseObject = SAMLMetaDataResponse.class, entityType = {})
  public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthenticator {
      private static final String s_name = "spmetadataresponse";
  
      @Inject
      ApiServerService _apiServer;
  
      SAML2AuthManager _samlAuthManager;
  
      /////////////////////////////////////////////////////
      /////////////// API Implementation///////////////////
      /////////////////////////////////////////////////////
  
      @Override
      public String getCommandName() {
          return s_name;
      }
  
      @Override
      public long getEntityOwnerId() {
          return Account.Type.NORMAL.ordinal();
      }
  
      @Override
      public void execute() throws ServerApiException {
          throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication plugin api, cannot be used directly");
      }
  
      @Override
      public String authenticate(String command, Map<String, Object[]> params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException {
          SAMLMetaDataResponse response = new SAMLMetaDataResponse();
          response.setResponseName(getCommandName());
  
          try {
              DefaultBootstrap.bootstrap();
          } catch (ConfigurationException | FactoryConfigurationError e) {
              logger.error("OpenSAML Bootstrapping error: " + e.getMessage());
              throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
                      "OpenSAML Bootstrapping error while creating SP MetaData",
                      params, responseType));
          }
  
          final SAMLProviderMetadata spMetadata = _samlAuthManager.getSPMetadata();
  
          EntityDescriptor spEntityDescriptor = new EntityDescriptorBuilder().buildObject();
          spEntityDescriptor.setEntityID(spMetadata.getEntityId());
  
          SPSSODescriptor spSSODescriptor = new SPSSODescriptorBuilder().buildObject();
          spSSODescriptor.setWantAssertionsSigned(true);
          spSSODescriptor.setAuthnRequestsSigned(true);
  
          X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory();
          keyInfoGeneratorFactory.setEmitEntityCertificate(true);
          KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance();
  
          KeyDescriptor signKeyDescriptor = new KeyDescriptorBuilder().buildObject();
          signKeyDescriptor.setUse(UsageType.SIGNING);
  
          KeyDescriptor encKeyDescriptor = new KeyDescriptorBuilder().buildObject();
          encKeyDescriptor.setUse(UsageType.ENCRYPTION);
  
          BasicX509Credential signingCredential = new BasicX509Credential();
          signingCredential.setEntityCertificate(spMetadata.getSigningCertificate());
  
          BasicX509Credential encryptionCredential = new BasicX509Credential();
          encryptionCredential.setEntityCertificate(spMetadata.getEncryptionCertificate());
  
          try {
              signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(signingCredential));
              encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(encryptionCredential));
              spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor);
              spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor);
          } catch (SecurityException e) {
              logger.warn("Unable to add SP X509 descriptors:" + e.getMessage());
          }
  
          NameIDFormat nameIDFormat = new NameIDFormatBuilder().buildObject();
          nameIDFormat.setFormat(NameIDType.PERSISTENT);
          spSSODescriptor.getNameIDFormats().add(nameIDFormat);
  
          NameIDFormat emailNameIDFormat = new NameIDFormatBuilder().buildObject();
          emailNameIDFormat.setFormat(NameIDType.EMAIL);
          spSSODescriptor.getNameIDFormats().add(emailNameIDFormat);
  
          NameIDFormat transientNameIDFormat = new NameIDFormatBuilder().buildObject();
          transientNameIDFormat.setFormat(NameIDType.TRANSIENT);
          spSSODescriptor.getNameIDFormats().add(transientNameIDFormat);
  
          AssertionConsumerService assertionConsumerService = new AssertionConsumerServiceBuilder().buildObject();
          assertionConsumerService.setIndex(1);
          assertionConsumerService.setIsDefault(true);
          assertionConsumerService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
          assertionConsumerService.setLocation(spMetadata.getSsoUrl());
          spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService);
  
          AssertionConsumerService assertionConsumerService2 = new AssertionConsumerServiceBuilder().buildObject();
          assertionConsumerService2.setIndex(2);
          assertionConsumerService2.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
          assertionConsumerService2.setLocation(spMetadata.getSsoUrl());
          spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService2);
  
          SingleLogoutService ssoService = new SingleLogoutServiceBuilder().buildObject();
          ssoService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
          ssoService.setLocation(spMetadata.getSloUrl());
          spSSODescriptor.getSingleLogoutServices().add(ssoService);
  
          SingleLogoutService ssoService2 = new SingleLogoutServiceBuilder().buildObject();
          ssoService2.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
          ssoService2.setLocation(spMetadata.getSloUrl());
          spSSODescriptor.getSingleLogoutServices().add(ssoService2);
  
          spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
          spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);
  
          // Add technical contact
          GivenName givenName = new GivenNameBuilder().buildObject();
          givenName.setName(spMetadata.getContactPersonName());
          EmailAddress emailAddress = new EmailAddressBuilder().buildObject();
          emailAddress.setAddress(spMetadata.getContactPersonEmail());
          ContactPerson contactPerson = new ContactPersonBuilder().buildObject();
          contactPerson.setType(ContactPersonTypeEnumeration.TECHNICAL);
          contactPerson.setGivenName(givenName);
          contactPerson.getEmailAddresses().add(emailAddress);
          spEntityDescriptor.getContactPersons().add(contactPerson);
  
          // Add administrative/support contact
          GivenName givenNameAdmin = new GivenNameBuilder().buildObject();
          givenNameAdmin.setName(spMetadata.getContactPersonName());
          EmailAddress emailAddressAdmin = new EmailAddressBuilder().buildObject();
          emailAddressAdmin.setAddress(spMetadata.getContactPersonEmail());
          ContactPerson contactPersonAdmin = new ContactPersonBuilder().buildObject();
          contactPersonAdmin.setType(ContactPersonTypeEnumeration.ADMINISTRATIVE);
          contactPersonAdmin.setGivenName(givenNameAdmin);
          contactPersonAdmin.getEmailAddresses().add(emailAddressAdmin);
          spEntityDescriptor.getContactPersons().add(contactPersonAdmin);
  
          Organization organization = new OrganizationBuilder().buildObject();
          OrganizationName organizationName = new OrganizationNameBuilder().buildObject();
          organizationName.setName(new LocalizedString(spMetadata.getOrganizationName(), Locale.getDefault().getLanguage()));
          OrganizationURL organizationURL = new OrganizationURLBuilder().buildObject();
          organizationURL.setURL(new LocalizedString(spMetadata.getOrganizationUrl(), Locale.getDefault().getLanguage()));
          organization.getOrganizationNames().add(organizationName);
          organization.getURLs().add(organizationURL);
          spEntityDescriptor.setOrganization(organization);
  
          StringWriter stringWriter = new StringWriter();
          try {
              DocumentBuilderFactory factory = ParserUtils.getSaferDocumentBuilderFactory();
              DocumentBuilder builder = factory.newDocumentBuilder();
              Document document = builder.newDocument();
              Marshaller out = Configuration.getMarshallerFactory().getMarshaller(spEntityDescriptor);
              out.marshall(spEntityDescriptor, document);
  
              Transformer transformer = ParserUtils.getSaferTransformerFactory().newTransformer();
              StreamResult streamResult = new StreamResult(stringWriter);
              DOMSource source = new DOMSource(document);
              transformer.transform(source, streamResult);
              stringWriter.close();
              response.setMetadata(stringWriter.toString());
          } catch (ParserConfigurationException | IOException | MarshallingException | TransformerException e) {
              if (responseType.equals(HttpUtils.JSON_CONTENT_TYPE)) {
                  response.setMetadata("Error creating Service Provider MetaData XML: " + e.getMessage());
              } else {
                  return "Error creating Service Provider MetaData XML: " + e.getMessage();
              }
          }
          // For JSON type return serialized response object
          if (responseType.equals(HttpUtils.RESPONSE_TYPE_JSON)) {
              return ApiResponseSerializer.toSerializedString(response, responseType);
          }
          // For other response types return XML
          return stringWriter.toString();
      }
  
      @Override
      public APIAuthenticationType getAPIType() {
          return APIAuthenticationType.READONLY_API;
      }
  
      @Override
      public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
          for (PluggableAPIAuthenticator authManager: authenticators) {
              if (authManager != null && authManager instanceof SAML2AuthManager) {
                  _samlAuthManager = (SAML2AuthManager) authManager;
              }
          }
          if (_samlAuthManager == null) {
              logger.error("No suitable Pluggable Authentication Manager found for SAML2 getSPMetadata Cmd");
          }
      }
  }
  