package com.intel.mtwilson.as.rest;
import com.intel.dcsg.cpg.validation.RegexPatterns;
import com.intel.mtwilson.as.data.TblHosts;
import com.intel.mtwilson.model.*;
import com.intel.dcsg.cpg.x509.repository.KeystoreCertificateRepository;
import java.security.cert.X509Certificate;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import com.intel.mtwilson.as.business.HostBO;
import com.intel.mountwilson.as.common.ASException;
import com.intel.mountwilson.as.common.ValidationException;
import com.intel.mtwilson.My;
import com.intel.mtwilson.as.ASComponentFactory;
import com.intel.mtwilson.i18n.ErrorCode;
import com.intel.mtwilson.datatypes.*;
import com.intel.mtwilson.security.annotations.*;
import java.io.IOException;
import com.intel.dcsg.cpg.validation.ValidationUtil;
import com.intel.mtwilson.launcher.ws.ext.V1;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.DefaultValue;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* REST Web Service *
*
* BACKWARD COMPATIBILITY:
* Some POST and PUT methods accept TxtHost2 or TxtHostRecord2 with extended fields.
* However, this class MUST NOT respond with those to the client because it would
* break v1 clients. The v1 contract does not allow extra fields.
* Therefore this class responds with TxtHost and TxtHostRecord.
*
*/
@V1
//@Stateless
@Path("/AttestationService/resources/hosts")
public class Host {
private HostBO hostBO = ASComponentFactory.getHostBO();
private Logger log = LoggerFactory.getLogger(getClass());
/**
* Returns the location of a host.
*
* Sample request: GET
* http://localhost:8080/AttestationService/resources/hosts/location?hostName=Some+TXT+Host
*
* Sample output: San Jose
*
* @param inputHostname unique name of the host to query
* @return the host location
*/
//@RolesAllowed({"Attestation", "Report"})
@RequiresPermissions("hosts:retrieve")
@GET
@Produces({MediaType.APPLICATION_JSON})
@Path("/location")
public HostLocation getLocation(@QueryParam("hostName") String hostName) {
if( !ValidationUtil.isValidWithRegex(hostName, RegexPatterns.IPADDR_FQDN) ) {throw new ValidationException("Invalid hostName parameter"); }
else return ASComponentFactory.getHostTrustBO().getHostLocation(new Hostname(hostName)); // datatype.Hostname
}
//@RolesAllowed({"Attestation", "Security"})
@RequiresPermissions("hosts:store")
@POST
@Produces({MediaType.APPLICATION_JSON})
@Path("/location")
public String addLocation(HostLocation hlObj) {
ValidationUtil.validate(hlObj);
Boolean result = ASComponentFactory.getHostTrustBO().addHostLocation(hlObj);
return Boolean.toString(result);
}
//@RolesAllowed({"Attestation","Report"})
@RequiresPermissions("host_attestations:create,retrieve")
@GET
@Produces({MediaType.APPLICATION_JSON})
@Path("/aik-{aik}/trust.json")
public HostTrustResponse getTrustByAik(@PathParam("aik")String aikFingerprint) {
ValidationUtil.validate(aikFingerprint);
if( aikFingerprint == null || aikFingerprint.isEmpty() ) { throw new ValidationException("Missing hostNames parameter"); }
else {
try {
// 0.5.1 returned MediaType.TEXT_PLAIN string like "BIOS:0,VMM:0" : return new HostTrustBO().getTrustStatusString(new Hostname(hostName)); // datatype.Hostname
Sha1Digest aikId = new Sha1Digest(aikFingerprint);
if( aikId.isValid() ) {
HostTrustStatus trust = ASComponentFactory.getHostTrustBO().getTrustStatusByAik(aikId);
return new HostTrustResponse(new Hostname(aikId.toString()), trust);
}
throw new ASException(ErrorCode.HTTP_INVALID_REQUEST, "Invalid AIK fingerprint: must be SHA1 digest");
}
catch(ASException e) {
throw e;
}catch(Exception ex) {
// throw new ASException(e);
log.error("Error during retrieval of host trust status.", ex);
throw new ASException(ErrorCode.AS_HOST_TRUST_ERROR, ex.getClass().getSimpleName());
}
}
}
//@RolesAllowed({"Attestation","Report"})
@RequiresPermissions("host_attestations:create,retrieve")
@GET
@Produces({"application/samlassertion+xml"})
@Path("/aik-{aik}/trust.saml")
public String getSamlByAik(
@PathParam("aik")String aikFingerprint,
@QueryParam("force_verify") @DefaultValue("false") Boolean forceVerify
) throws IOException {
try {
// 0.5.1 returned MediaType.TEXT_PLAIN string like "BIOS:0,VMM:0" : return new HostTrustBO().getTrustStatusString(new Hostname(hostName)); // datatype.Hostname
Sha1Digest aikId = new Sha1Digest(aikFingerprint);
if( aikId.isValid() ) {
return ASComponentFactory.getHostTrustBO().getTrustWithSamlByAik(aikId, forceVerify);
}
throw new ASException(ErrorCode.HTTP_INVALID_REQUEST, "Invalid AIK fingerprint: must be SHA1 digest");
}
catch(ASException e) {
throw e;
}catch(Exception e) {
throw new ASException(e);
}
}
/**
* Implements SAFE AR "closing the loop" aka "tls-enforced attestation" and "host bind-data public key".
* Given SHA1(AIK), returns DER-ENCODED X509 CERTIFICATE corresponding to the host in its current trust
* policy -- even if the host is not currently trusted, the returned certificate corresponds to the current
* trust policy for the host so that when the host becomes trusted the certificate will be valid.
* If the whitelist for the host changes you will have to obtain a new certificate.
* @author jbuhacoff
* @since 1.2
* @param aikFingerprint
* @return
*/
//@RolesAllowed({"Attestation","Report"})
@RequiresPermissions("host_aik_certificates:retrieve")
@GET
@Produces({MediaType.APPLICATION_OCTET_STREAM})
@Path("/aik-{aik}/trustcert.x509")
public byte[] getCurrentTrustCertificateByAik(@PathParam("aik")String aikFingerprint) {
ValidationUtil.validate(aikFingerprint);
if( aikFingerprint == null || aikFingerprint.isEmpty() ) { throw new ValidationException("Missing hostNames parameter"); }
else{
try {
// 0.5.1 returned MediaType.TEXT_PLAIN string like "BIOS:0,VMM:0" : return new HostTrustBO().getTrustStatusString(new Hostname(hostName)); // datatype.Hostname
Sha1Digest aikId = new Sha1Digest(aikFingerprint);
if( aikId.isValid() ) {
TblHosts host = ASComponentFactory.getHostBO().getHostByAik(aikId);
KeystoreCertificateRepository repository = new KeystoreCertificateRepository(host.getTlsKeystoreResource(),My.configuration().getTlsKeystorePassword());
List<X509Certificate> certificates = repository.getCertificates(); // guaranteed not to be null, but may be empty
for(X509Certificate certificate : certificates) {
// we are looking for a certificate that is marked with the AIK; the other one is the trust agent's or vcenter's ssl; for now we are putting the AIK fingerprint in the subject CN
if( certificate.getSubjectX500Principal().getName().contains("CN="+aikId.toString()) ) {
return certificate.getEncoded();
}
}
throw new ASException(ErrorCode.HTTP_NOT_FOUND, "Host does not have a certificate under that AIK");
}
throw new ASException(ErrorCode.HTTP_INVALID_REQUEST, "Invalid AIK fingerprint: must be SHA1 digest");
}
catch(ASException e) {
throw e;
}catch(Exception ex) {
// throw new ASException(e);
log.error("Error during retrieval of host trust certificate.", ex);
throw new ASException(ErrorCode.AS_HOST_TRUST_CERT_ERROR, ex.getClass().getSimpleName());
}
}
}
/**
* Adds a new host to the database. This action involves contacting the host
* to obtain trust-related information. If the host is offline, the request
* will fail.
*
* Required parameters for all hosts are host name, BIOS name and version,
* and VMM name and version. If the host is an ESX host then the vCenter
* connection URL is also required. Otherwise, the host IP address and port
* are required. Host description and contact email are optional.
*
* Parameter names:
* HostName
* IPAddress
* Port
* BIOS_Name
* BIOS_Version
* BIOS_Oem
* VMM_Name
* VMM_Version
* VMM_OSName
* VMM_OSVersion
* AddOn_Connection_String
* Description
* Email
*
* @param host a form containing the above parameters
* @return error status
*
* Response:
* {"error_code":"",error_message:""}
*/
//@RolesAllowed({"Attestation"})
@RequiresPermissions("hosts:create")
@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public HostResponse post(TxtHostRecord hostRecord) {
ValidationUtil.validate(hostRecord);
if( hostRecord == null || hostRecord.HostName == null || hostRecord.HostName.isEmpty() ) { throw new ValidationException("Missing hostNames parameter"); }
else return hostBO.addHost(new TxtHost(hostRecord), null, null, null);
}
/**
* Updates an existing host in the database. This action involves contacting
* the host to obtain trust-related information. If the host is offline, the
* request will fail.
*
* Required parameters for all hosts are host name, BIOS name and version,
* and VMM name and version. If the host is an ESX host then the vCenter
* connection URL is also required. Otherwise, the host IP address and port
* are required. Host description and contact email are optional.
*
* Parameter names:
* HostName
* IPAddress
* Port
* BIOS_Name
* BIOS_Version
* VMM_Name
* VMM_Version
* AddOn_Connection_String
* Description
* Email
*
*
* @param host a form containing the above parameters
* @return error status
*/
//@RolesAllowed({"Attestation"})
@RequiresPermissions("hosts:store")
@PUT
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public HostResponse put(TxtHostRecord hostRecord) {
ValidationUtil.validate(hostRecord);
if( hostRecord == null || hostRecord.HostName == null || hostRecord.HostName.isEmpty() ) { throw new ValidationException("Missing hostNames parameter"); }
else return hostBO.updateHost(new TxtHost(hostRecord), null, null, null);
}
/**
* Returns the trust status of a host.
*
* Sample request: GET
* http://localhost:8080/AttestationService/resources/hosts/trust?hostName=Some+TXT+Host
*
* Sample output for untrusted host: BIOS:0,VMM:0
*
* Sample output for trusted host: BIOS:1,VMM:1
*
* @param hostName unique name of the host to query
* @param forceVerify
* @return a string like BIOS:0,VMM:0 representing the trust status
*/
//@RolesAllowed({"Attestation", "Report"})
@RequiresPermissions({"host_attestations:retrieve"})
@GET
@Produces({MediaType.APPLICATION_JSON})
@Path("/trust")
public HostTrustResponse get(
@QueryParam("hostName") String hostName,
@QueryParam("force_verify") @DefaultValue("false") Boolean forceVerify) throws ASException {
if( !ValidationUtil.isValidWithRegex(hostName, RegexPatterns.IPADDR_FQDN) ) {throw new ValidationException("Invalid hostName parameter"); }
try {
// 0.5.1 returned MediaType.TEXT_PLAIN string like "BIOS:0,VMM:0" : return new HostTrustBO().getTrustStatusString(new Hostname(hostName)); // datatype.Hostname
Hostname hostname = new Hostname(hostName);
if(hostname.isValid()){
//HostTrustStatus trust = ASComponentFactory.getHostTrustBO().getTrustStatus(hostname);
HostTrustStatus trust = ASComponentFactory.getHostTrustBO().getTrustStatusWithCache(hostName, forceVerify);
return new HostTrustResponse(hostname, trust);
}
else throw new ASException(ErrorCode.AS_MISSING_INPUT, "hostName");
} catch (ASException e) {
throw e;
} catch (Exception ex) {
// throw new ASException(e);
log.error("Error during retrieval of host trust status.", ex);
throw new ASException(ErrorCode.AS_HOST_TRUST_ERROR, ex.getClass().getSimpleName());
}
}
/**
* Deletes a host from the database.
*
* Example request: DELETE
* http://localhost:8080/AttestationService/resources/hosts?hostName=Some+TXT+Host
*
* @param hostName the unique host name of the host to delete
* @return error status
*/
//@RolesAllowed({"Attestation"})
@RequiresPermissions("hosts:delete")
@DELETE
// @Consumes({"text/html"})
@Produces({MediaType.APPLICATION_JSON})
public HostResponse delete(@QueryParam("hostName") String hostName) {
if( !ValidationUtil.isValidWithRegex(hostName, RegexPatterns.IPADDR_FQDN) ) {throw new ValidationException("Invalid hostName parameter"); }
else return hostBO.deleteHost(new Hostname(hostName), null);
}
/**
*
* @param searchCriteria optional, a string that would be contained in the host name; if not specified you will get a list of all the hosts
* @return list of hosts whose hostname contains the value specified by searchCriteria; in SQL terms, WHERE hostname LIKE '%searchCriteria%'
*/
//@RolesAllowed({"Attestation", "Report", "Security"})
@RequiresPermissions("hosts:search")
@GET
@Produces({MediaType.APPLICATION_JSON})
public List<TxtHostRecord> queryForHosts(
@QueryParam("searchCriteria") String searchCriteria,
@QueryParam("includeHardwareUuid") @DefaultValue("false") boolean includeHardwareUuid,
@QueryParam("includeTlsPolicy") @DefaultValue("false") boolean includeTlsPolicy) {
log.debug("queryForHosts api searchCriteria["+searchCriteria+"] ");
ValidationUtil.validate(searchCriteria);
//if( searchCriteria == null || searchCriteria.isEmpty() ) { throw new ValidationException("Missing hostNames parameter"); }
//else
List<TxtHostRecord> resultset;
if(includeHardwareUuid) {
resultset = hostBO.queryForHosts(searchCriteria,includeHardwareUuid);
}else{
resultset = hostBO.queryForHosts(searchCriteria);
}
if( !includeTlsPolicy ) {
for(TxtHostRecord record : resultset) {
record.tlsPolicyChoice = null;
}
}
return resultset;
}
// The following 2 APIs were being used internally. Since we are directly calling into the Attestation layer BO from the Management service
// we do not need this API anymore.
//@RolesAllowed({"Attestation"})
// @RequiresPermissions({"hosts:create","mles:search"})
// @POST
// @Consumes({MediaType.APPLICATION_JSON})
// @Produces({MediaType.APPLICATION_JSON})
// @Path("/mle")
// public HostResponse registerHostByFindingMLE(TxtHostRecord hostRecord) {
// ValidationUtil.validate(hostRecord);
// return ASComponentFactory.getHostTrustBO().getTrustStatusOfHostNotInDBAndRegister(hostRecord);
// }
//
// //@RolesAllowed({"Attestation"})
// @RequiresPermissions({"hosts:create","mles:search"})
// @POST
// @Consumes({MediaType.APPLICATION_JSON})
// @Produces({MediaType.APPLICATION_JSON})
// @Path("/mle/verify")
// public String checkMatchingMLEExists(TxtHostRecord hostRecord) {
// ValidationUtil.validate(hostRecord);
// String result = ASComponentFactory.getHostTrustBO().checkMatchingMLEExists(hostRecord,
// hostRecord.Location.substring(0, hostRecord.Location.indexOf("|")), hostRecord.Location.substring(hostRecord.Location.indexOf("|")+1));
// System.out.println("checkMatchingMLEExists RESULT:" + result);
// return result;
// }
}