From 881f700848aafda35f5471efd31a1726eca5c74c Mon Sep 17 00:00:00 2001 From: Stelios Voutsinas Date: Wed, 19 Jun 2024 18:11:09 -0700 Subject: [PATCH] Added results servlet / deprecated Authenticator / Added Capabilities fix --- CHANGELOG.md | 11 + .../ca/nrc/cadc/sample/AuthenticatorImpl.java | 10 +- .../java/ca/nrc/cadc/sample/CapGetAction.java | 232 ++++++++++++++++++ .../ca/nrc/cadc/sample/CapInitAction.java | 208 ++++++++++++++++ .../ca/nrc/cadc/sample/ResultStoreImpl.java | 9 +- .../ca/nrc/cadc/sample/ResultsServlet.java | 50 ++++ tap/src/main/webapp/WEB-INF/web.xml | 27 +- 7 files changed, 534 insertions(+), 13 deletions(-) create mode 100644 tap/src/main/java/ca/nrc/cadc/sample/CapGetAction.java create mode 100644 tap/src/main/java/ca/nrc/cadc/sample/CapInitAction.java create mode 100644 tap/src/main/java/ca/nrc/cadc/sample/ResultsServlet.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e90643e..fc4b3f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Find changes for the upcoming release in the project's [changelog.d](https://git + +## 1.18.0 (2024-06-24) + +### Changed + +- Change result handling, to use a redirect servlet. Addresses issue with async failing due to auth header propagation with clients like pyvo, topcat + +### Fixed + +- Fixed Capabilities handling. Use new CapGetAction & CapInitAction, modified by getting pathPrefix from ENV property + ## 1.17.3 (2024-06-18) diff --git a/tap/src/main/java/ca/nrc/cadc/sample/AuthenticatorImpl.java b/tap/src/main/java/ca/nrc/cadc/sample/AuthenticatorImpl.java index 3ff799f..a3f2839 100644 --- a/tap/src/main/java/ca/nrc/cadc/sample/AuthenticatorImpl.java +++ b/tap/src/main/java/ca/nrc/cadc/sample/AuthenticatorImpl.java @@ -29,14 +29,10 @@ import org.apache.log4j.Logger; /** - * Implementes the Authenticator for processing Gafaelfawr auth, - * and using it to authenticate against the TAP service. - * - * The token in the authorization header is used to make a call - * to Gafaelfawr to retrieve details such as the uid and uidNumber. - * - * @author cbanek + * @deprecated This class is deprecated and will be removed in future releases. + * The TAP Service now uses IdentityManager for authentication, available in the opencadc library */ +@Deprecated public class AuthenticatorImpl implements Authenticator { private static final Logger log = Logger.getLogger(AuthenticatorImpl.class); diff --git a/tap/src/main/java/ca/nrc/cadc/sample/CapGetAction.java b/tap/src/main/java/ca/nrc/cadc/sample/CapGetAction.java new file mode 100644 index 0000000..11312c2 --- /dev/null +++ b/tap/src/main/java/ca/nrc/cadc/sample/CapGetAction.java @@ -0,0 +1,232 @@ +/* +************************************************************************ +******************* CANADIAN ASTRONOMY DATA CENTRE ******************* +************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** +* +* (c) 2020. (c) 2020. +* Government of Canada Gouvernement du Canada +* National Research Council Conseil national de recherches +* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +* All rights reserved Tous droits réservés +* +* NRC disclaims any warranties, Le CNRC dénie toute garantie +* expressed, implied, or énoncée, implicite ou légale, +* statutory, of any kind with de quelque nature que ce +* respect to the software, soit, concernant le logiciel, +* including without limitation y compris sans restriction +* any warranty of merchantability toute garantie de valeur +* or fitness for a particular marchande ou de pertinence +* purpose. NRC shall not be pour un usage particulier. +* liable in any event for any Le CNRC ne pourra en aucun cas +* damages, whether direct or être tenu responsable de tout +* indirect, special or general, dommage, direct ou indirect, +* consequential or incidental, particulier ou général, +* arising from the use of the accessoire ou fortuit, résultant +* software. Neither the name de l'utilisation du logiciel. Ni +* of the National Research le nom du Conseil National de +* Council of Canada nor the Recherches du Canada ni les noms +* names of its contributors may de ses participants ne peuvent +* be used to endorse or promote être utilisés pour approuver ou +* products derived from this promouvoir les produits dérivés +* software without specific prior de ce logiciel sans autorisation +* written permission. préalable et particulière +* par écrit. +* +* This file is part of the Ce fichier fait partie du projet +* OpenCADC project. OpenCADC. +* +* OpenCADC is free software: OpenCADC est un logiciel libre ; +* you can redistribute it and/or vous pouvez le redistribuer ou le +* modify it under the terms of modifier suivant les termes de +* the GNU Affero General Public la “GNU Affero General Public +* License as published by the License” telle que publiée +* Free Software Foundation, par la Free Software Foundation +* either version 3 of the : soit la version 3 de cette +* License, or (at your option) licence, soit (à votre gré) +* any later version. toute version ultérieure. +* +* OpenCADC is distributed in the OpenCADC est distribué +* hope that it will be useful, dans l’espoir qu’il vous +* but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +* without even the implied GARANTIE : sans même la garantie +* warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +* or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF +* PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +* General Public License for Générale Publique GNU Affero +* more details. pour plus de détails. +* +* You should have received Vous devriez avoir reçu une +* a copy of the GNU Affero copie de la Licence Générale +* General Public License along Publique GNU Affero avec +* with OpenCADC. If not, see OpenCADC ; si ce n’est +* . pas le cas, consultez : +* . +* +************************************************************************ +*/ + +package ca.nrc.cadc.sample; + +import ca.nrc.cadc.auth.AuthMethod; +import ca.nrc.cadc.auth.AuthenticationUtil; +import ca.nrc.cadc.auth.NotAuthenticatedException; +import ca.nrc.cadc.net.HttpTransfer; +import ca.nrc.cadc.net.ResourceNotFoundException; +import ca.nrc.cadc.reg.AccessURL; +import ca.nrc.cadc.reg.Capabilities; +import ca.nrc.cadc.reg.CapabilitiesWriter; +import ca.nrc.cadc.reg.Capability; +import ca.nrc.cadc.reg.Interface; +import ca.nrc.cadc.reg.Standards; +import ca.nrc.cadc.reg.client.LocalAuthority; +import ca.nrc.cadc.reg.client.RegistryClient; +import ca.nrc.cadc.rest.InlineContentHandler; +import ca.nrc.cadc.rest.RestAction; +import ca.nrc.cadc.rest.SyncOutput; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Set; +import java.util.TreeSet; +import ca.nrc.cadc.sample.CapInitAction; +import org.apache.log4j.Logger; + +/** + * + * @author pdowler + */ +public class CapGetAction extends RestAction { + private static final Logger log = Logger.getLogger(CapGetAction.class); + private static final String baseURL = System.getProperty("base_url"); + private static final String pathPrefix = System.getProperty("path_prefix"); + + /** + * Enable transformation of the capabilities template (default: true). Subclasses + * may disable this according to some policy. The current transform is to change + * the host name in every accessURL in the capabilities to match the host name used + * in the request. This works fine in most cases but would not work + * if some accessURL(s) within an application are deployed on a different host. + * For example, if the VOSI-availability endpoint is deployed on an separate host + * so it can probe the service from the outside, then capabilities transform + * would need to be disabled. + */ + protected boolean doTransform = true; + + public CapGetAction() { + super(); + } + + @Override + protected String getServerImpl() { + return CapInitAction.getVersion(componentID); + } + + @Override + protected InlineContentHandler getInlineContentHandler() { + return null; + } + + @Override + public void doAction() throws Exception { + if (CapInitAction.getAuthRequired(componentID)) { + AuthMethod am = AuthenticationUtil.getAuthMethod(AuthenticationUtil.getCurrentSubject()); + if (am == null || am.equals(AuthMethod.ANON)) { + throw new NotAuthenticatedException("permission denied"); + } + } + + Capabilities caps = CapInitAction.getTemplate(componentID); + + log.debug("transformAccessURL=" + doTransform); + + if (doTransform) { + transform(caps); + } + + doOutput(caps, syncOutput); + logInfo.setSuccess(true); + } + + // transform all accessURL so the hostname and context path match that used to invoke + // the /capabilities endpoint + private void transform(Capabilities caps) throws MalformedURLException { + log.debug("context: " + syncInput.getContextPath()); + log.debug("component: " + syncInput.getComponentPath()); + + String hostname = new URL(syncInput.getRequestURI()).getHost(); + + // find context path in the template using capabilities endpoint + Capability cap = caps.findCapability(Standards.VOSI_CAPABILITIES); + URL capURL = cap.getInterfaces().get(0).getAccessURL().getURL(); + String capPath = capURL.getPath(); + String basePath = capURL.getPath().substring(0, capPath.indexOf("/capabilities")); // chop + + // capabilities in the request + String actualPath = syncInput.getContextPath() + syncInput.getComponentPath(); + actualPath = actualPath.substring(0, actualPath.indexOf("/capabilities")); // chop + + log.debug("transform: basePath in template: " + basePath + " actualPath: " + actualPath); + for (Capability c : caps.getCapabilities()) { + for (Interface i : c.getInterfaces()) { + AccessURL u = i.getAccessURL(); + URL url = u.getURL(); + String path = url.getPath(); + String npath = path.replace(basePath, pathPrefix); + URL nurl = new URL(url.getProtocol(), hostname, npath); + u.setURL(nurl); + log.debug("transform: " + url + " -> " + nurl); + } + } + } + + private void doOutput(Capabilities caps, SyncOutput out) throws IOException { + out.setHeader(HttpTransfer.CONTENT_TYPE, "text/xml"); + out.setCode(200); + CapabilitiesWriter w = new CapabilitiesWriter(); + w.write(caps, syncOutput.getOutputStream()); + } + + + private void injectAuthProviders(Capabilities caps) throws IOException { + Set sms = new TreeSet<>(); + for (Capability cap : caps.getCapabilities()) { + for (Interface i : cap.getInterfaces()) { + for (URI s : i.getSecurityMethods()) { + sms.add(s); + } + } + + } + log.debug("found " + sms.size() + " unique SecurityMethod(s)"); + if (sms.isEmpty()) { + return; + } + + LocalAuthority loc = new LocalAuthority(); + RegistryClient reg = new RegistryClient(); + for (URI sm : sms) { + URI resourceID = loc.getServiceURI(sm.toASCIIString()); + try { + if (resourceID != null) { + Capabilities srv = reg.getCapabilities(resourceID); + if (srv != null) { + Capability auth = srv.findCapability(sm); + if (auth != null) { + caps.getCapabilities().add(auth); + } else { + log.debug("not found: " + sm + " in " + resourceID); + } + } else { + log.debug("not found: " + resourceID + " capabilities"); + } + } else { + log.debug("not found: " + sm); + } + } catch (ResourceNotFoundException ex) { + log.warn("failed to find auth service: " + resourceID + "cause: " + ex); + } + } + } + +} \ No newline at end of file diff --git a/tap/src/main/java/ca/nrc/cadc/sample/CapInitAction.java b/tap/src/main/java/ca/nrc/cadc/sample/CapInitAction.java new file mode 100644 index 0000000..73ef529 --- /dev/null +++ b/tap/src/main/java/ca/nrc/cadc/sample/CapInitAction.java @@ -0,0 +1,208 @@ +/* +************************************************************************ +******************* CANADIAN ASTRONOMY DATA CENTRE ******************* +************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** +* +* (c) 2024. (c) 2024. +* Government of Canada Gouvernement du Canada +* National Research Council Conseil national de recherches +* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +* All rights reserved Tous droits réservés +* +* NRC disclaims any warranties, Le CNRC dénie toute garantie +* expressed, implied, or énoncée, implicite ou légale, +* statutory, of any kind with de quelque nature que ce +* respect to the software, soit, concernant le logiciel, +* including without limitation y compris sans restriction +* any warranty of merchantability toute garantie de valeur +* or fitness for a particular marchande ou de pertinence +* purpose. NRC shall not be pour un usage particulier. +* liable in any event for any Le CNRC ne pourra en aucun cas +* damages, whether direct or être tenu responsable de tout +* indirect, special or general, dommage, direct ou indirect, +* consequential or incidental, particulier ou général, +* arising from the use of the accessoire ou fortuit, résultant +* software. Neither the name de l'utilisation du logiciel. Ni +* of the National Research le nom du Conseil National de +* Council of Canada nor the Recherches du Canada ni les noms +* names of its contributors may de ses participants ne peuvent +* be used to endorse or promote être utilisés pour approuver ou +* products derived from this promouvoir les produits dérivés +* software without specific prior de ce logiciel sans autorisation +* written permission. préalable et particulière +* par écrit. +* +* This file is part of the Ce fichier fait partie du projet +* OpenCADC project. OpenCADC. +* +* OpenCADC is free software: OpenCADC est un logiciel libre ; +* you can redistribute it and/or vous pouvez le redistribuer ou le +* modify it under the terms of modifier suivant les termes de +* the GNU Affero General Public la “GNU Affero General Public +* License as published by the License” telle que publiée +* Free Software Foundation, par la Free Software Foundation +* either version 3 of the : soit la version 3 de cette +* License, or (at your option) licence, soit (à votre gré) +* any later version. toute version ultérieure. +* +* OpenCADC is distributed in the OpenCADC est distribué +* hope that it will be useful, dans l’espoir qu’il vous +* but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +* without even the implied GARANTIE : sans même la garantie +* warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +* or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF +* PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +* General Public License for Générale Publique GNU Affero +* more details. pour plus de détails. +* +* You should have received Vous devriez avoir reçu une +* a copy of the GNU Affero copie de la Licence Générale +* General Public License along Publique GNU Affero avec +* with OpenCADC. If not, see OpenCADC ; si ce n’est +* . pas le cas, consultez : +* . +* +************************************************************************ +*/ + +package ca.nrc.cadc.sample; + +import ca.nrc.cadc.reg.Capabilities; +import ca.nrc.cadc.reg.CapabilitiesReader; +import ca.nrc.cadc.rest.InitAction; +import ca.nrc.cadc.rest.Version; +import ca.nrc.cadc.util.StringUtil; + +import java.io.StringReader; +import java.net.URL; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.apache.log4j.Logger; + +/** + * InitAction implementation for VOSI-capabilities from template xml file. + * + * @author pdowler + */ +public class CapInitAction extends InitAction { + private static final Logger log = Logger.getLogger(CapInitAction.class); + + public CapInitAction() { + super(); + } + + static Capabilities getTemplate(String componentID) { + String jndiKey = componentID + ".cap-template"; + try { + log.debug("retrieving capabilities template via JNDI: " + jndiKey); + Context initContext = new InitialContext(); + String tmpl = (String) initContext.lookup(jndiKey); + CapabilitiesReader cr = new CapabilitiesReader(false); // validated in doInit + StringReader sr = new StringReader(tmpl); + Capabilities caps = cr.read(sr); + return caps; + } catch (Exception ex) { + throw new IllegalStateException("failed to find template via JNDI: init failed", ex); + } + } + + static boolean getAuthRequired(String componentID) { + String jndiKey = componentID + ".authRequired"; + try { + log.debug("retrieving authRequired via JNDI: " + jndiKey); + Context initContext = new InitialContext(); + Boolean authRequired = (Boolean) initContext.lookup(jndiKey); + if (authRequired == null) { + return false; + } + return authRequired; + } catch (Exception ex) { + throw new IllegalStateException("failed to find authRequired via JNDI: init failed", ex); + } + } + + static String getVersion(String componentID) { + String jndiKey = componentID + ".version"; + try { + log.debug("retrieving version via JNDI: " + jndiKey); + Context initContext = new InitialContext(); + String version = (String) initContext.lookup(jndiKey); + return version; + } catch (Exception ex) { + throw new IllegalStateException("failed to find version via JNDI: init failed", ex); + } + } + + @Override + public void doInit() { + + final Context initContext; + try { + initContext = new InitialContext(); + } catch (NamingException ex) { + throw new IllegalStateException("failed to find JDNI InitialContext", ex); + } + + String jndiKey = componentID + ".cap-template"; + String str = initParams.get("input"); + + log.debug("doInit: static capabilities: " + str); + try { + URL resURL = super.getResource(str); + String tmpl = StringUtil.readFromInputStream(resURL.openStream(), "UTF-8"); + + // validate + CapabilitiesReader cr = new CapabilitiesReader(); + cr.read(tmpl); + try { + log.debug("unbinding possible existing template"); + initContext.unbind(jndiKey); + } catch (NamingException e) { + log.debug("no previously bound template, continuting"); + } + initContext.bind(jndiKey, tmpl); + log.info("doInit: capabilities template=" + str + " stored via JNDI: " + jndiKey); + } catch (Exception ex) { + throw new IllegalArgumentException("CONFIG: failed to read capabilities template: " + str, ex); + } + + try { + String authRequired = initParams.get("authRequired"); + jndiKey = componentID + ".authRequired"; + try { + log.debug("unbinding possible authRequired value"); + initContext.unbind(jndiKey); + } catch (NamingException e) { + log.debug("no previously bound value, continuting"); + } + if ("true".equals(authRequired)) { + initContext.bind(jndiKey, Boolean.TRUE); + log.info("doInit: authRequired=true stored via JNDI: " + jndiKey); + } else { + initContext.bind(jndiKey, Boolean.FALSE); + log.info("doInit: authRequired=false stored via JNDI: " + jndiKey); + } + } catch (Exception ex) { + throw new IllegalArgumentException("CONFIG: failed to set authRequired flag", ex); + } + + try { + Version version = getLibraryVersion(CapInitAction.class); + + jndiKey = componentID + ".version"; + try { + log.debug("unbinding possible version value"); + initContext.unbind(jndiKey); + } catch (NamingException e) { + log.debug("no previously bound value, continuting"); + } + initContext.bind(jndiKey, version.getMajorMinor()); + log.info("doInit: version=" + version + " stored via JNDI: " + jndiKey); + } catch (Exception ex) { + throw new IllegalArgumentException("CONFIG: failed to set version flag", ex); + } + } +} \ No newline at end of file diff --git a/tap/src/main/java/ca/nrc/cadc/sample/ResultStoreImpl.java b/tap/src/main/java/ca/nrc/cadc/sample/ResultStoreImpl.java index 0298e23..7149a04 100644 --- a/tap/src/main/java/ca/nrc/cadc/sample/ResultStoreImpl.java +++ b/tap/src/main/java/ca/nrc/cadc/sample/ResultStoreImpl.java @@ -1,4 +1,3 @@ - /* ************************************************************************ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* @@ -94,7 +93,8 @@ public class ResultStoreImpl implements ResultStore { private String filename; private static final String bucket = System.getProperty("gcs_bucket"); private static final String bucketURL = System.getProperty("gcs_bucket_url"); - + private static final String baseURL = System.getProperty("base_url"); + private static final String pathPrefix = System.getProperty("path_prefix"); @Override public URL put(final ResultSet resultSet, @@ -135,14 +135,14 @@ public URL put(final ResultSet resultSet, private OutputStream getOutputStream() { Storage storage = StorageOptions.getDefaultInstance().getService(); BlobId blobId = BlobId.of(bucket, filename); + BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType("application/x-votable+xml").build(); Blob blob = storage.create(blobInfo); return Channels.newOutputStream(blob.writer()); } private URL getURL() throws MalformedURLException { - URL bucket = new URL(bucketURL); - return new URL(bucket, filename); + return new URL(baseURL + pathPrefix + "/results/" + filename); } @Override @@ -157,4 +157,5 @@ public void setJob(Job _job) { public void setFilename(String filename) { this.filename = filename; } + } diff --git a/tap/src/main/java/ca/nrc/cadc/sample/ResultsServlet.java b/tap/src/main/java/ca/nrc/cadc/sample/ResultsServlet.java new file mode 100644 index 0000000..f18d0b4 --- /dev/null +++ b/tap/src/main/java/ca/nrc/cadc/sample/ResultsServlet.java @@ -0,0 +1,50 @@ +package ca.nrc.cadc.sample; + +import org.apache.log4j.Logger; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A servlet that handles redirecting to specific job results. + * This servlet extracts the VOTable file name from the request path and constructs a URL to redirect the client. + * + * @author stvoutsin + */ +public class ResultsServlet extends HttpServlet { + private static final Logger log = Logger.getLogger(ResultsServlet.class); + private static final String bucketURL = System.getProperty("gcs_bucket_url"); + + /** + * Processes GET requests by extracting the result filename from the request path and redirecting to the corresponding results URL. + * The filename is assumed to be the path info of the request URL, following the first '/' character. + * + * @param request the HttpServletRequest object that contains the request + * @param response the HttpServletResponse object that contains the response + * @throws ServletException if an input or output error is detected when the servlet handles the GET request + * @throws IOException if the request for the GET could not be handled + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + try { + String path = request.getPathInfo(); + String redirectUrl = generateRedirectUrl(bucketURL, path); + response.sendRedirect(redirectUrl); + } catch (Exception e) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "An error occurred while processing the request."); + } + } + + /** + * Generates the redirect URL based on a path. + * + * @param path the request path + * @return the redirect URL constructed using the bucket URL and results file + */ + private String generateRedirectUrl(String bucketUrlString, String path) { + String resultsFile = path.substring(1); + return bucketUrlString + "/" + resultsFile; + } +} diff --git a/tap/src/main/webapp/WEB-INF/web.xml b/tap/src/main/webapp/WEB-INF/web.xml index 71a4aa6..f8d1d1f 100644 --- a/tap/src/main/webapp/WEB-INF/web.xml +++ b/tap/src/main/webapp/WEB-INF/web.xml @@ -170,10 +170,22 @@ - + CapabilitiesServlet - ca.nrc.cadc.vosi.CapabilitiesServlet + ca.nrc.cadc.rest.RestServlet + + init + ca.nrc.cadc.sample.CapInitAction + + + head + ca.nrc.cadc.vosi.CapHeadAction + + + get + ca.nrc.cadc.sample.CapGetAction + input /capabilities.xml @@ -193,6 +205,17 @@ SyncServlet /sync/* + + + ResultsServlet + ca.nrc.cadc.sample.ResultsServlet + + + + ResultsServlet + /results/* + + AsyncServlet /async/*