blob: e462e33cbfebb4148d137e5e014e818041693ad0 [file] [log] [blame]
// 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.apache.log4j.Logger;
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 {
public static final Logger s_logger = Logger.getLogger(GetServiceProviderMetaDataCmd.class.getName());
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) {
s_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) {
s_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) {
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 getSPMetadata Cmd");
}
}
}