diff --git a/youcat/build.gradle b/youcat/build.gradle index 14567411..e9defe8e 100644 --- a/youcat/build.gradle +++ b/youcat/build.gradle @@ -31,7 +31,7 @@ dependencies { implementation 'org.opencadc:cadc-uws:[1.0.2,)' implementation 'org.opencadc:cadc-uws-server:[1.2.22,)' implementation 'org.opencadc:cadc-tap:[1.1.20,)' - implementation 'org.opencadc:cadc-tap-schema:[1.2.7,)' + implementation 'org.opencadc:cadc-tap-schema:[1.2.8,)' implementation 'org.opencadc:cadc-tap-server:[1.1.32,)' implementation 'org.opencadc:cadc-tap-server-pg:[1.1.1,)' implementation 'org.opencadc:cadc-adql:[1.1.4,)' diff --git a/youcat/src/intTest/java/org/opencadc/youcat/TemplateDAOTest.java b/youcat/src/intTest/java/org/opencadc/youcat/TemplateDAOTest.java new file mode 100644 index 00000000..1952d835 --- /dev/null +++ b/youcat/src/intTest/java/org/opencadc/youcat/TemplateDAOTest.java @@ -0,0 +1,208 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2025. (c) 2025. + * 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 : + * . + * + * : 5 $ + * + ************************************************************************ + */ + +package org.opencadc.youcat; + +import ca.nrc.cadc.auth.AuthenticationUtil; +import ca.nrc.cadc.auth.SSLUtil; +import ca.nrc.cadc.dali.tables.votable.VOTableGroup; +import ca.nrc.cadc.dali.tables.votable.VOTableResource; +import ca.nrc.cadc.db.ConnectionConfig; +import ca.nrc.cadc.db.DBConfig; +import ca.nrc.cadc.db.DBUtil; +import ca.nrc.cadc.util.FileUtil; +import ca.nrc.cadc.util.Log4jInit; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import javax.security.auth.Subject; +import javax.sql.DataSource; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.opencadc.datalink.ServiceDescriptorTemplate; +import org.springframework.jdbc.core.JdbcTemplate; + +public class TemplateDAOTest { + private static final Logger log = Logger.getLogger(TemplateDAOTest.class); + + static { + Log4jInit.setLevel("org.opencadc.youcat", Level.DEBUG); + Log4jInit.setLevel("ca.nrc.cadc.tap", Level.DEBUG); + Log4jInit.setLevel("ca.nrc.cadc.db.version", Level.DEBUG); + } + + static final String OWNER_CERT = "youcat-owner.pem"; // own test schema + static final String MEMBER_CERT = "youcat-member.pem"; // member of group + + final Subject owner; + final Subject member; + final DataSource dataSource; + + @Before + public void setUp() throws Exception { + JdbcTemplate jdbc = new JdbcTemplate(dataSource); + String sql = "DELETE FROM tap_schema.ServiceDescriptorTemplate"; + jdbc.update(sql); + log.debug(sql); + } + + public TemplateDAOTest() { + try { + DBConfig conf = new DBConfig(); + ConnectionConfig cc = conf.getConnectionConfig("TAP_SCHEMA_TEST", "cadctest"); + dataSource = DBUtil.getDataSource(cc, true, true); + log.info("configured data source: " + cc.getServer() + "," + cc.getDatabase() + "," + cc.getDriver() + "," + cc.getURL()); + + File cert = FileUtil.getFileFromResource(OWNER_CERT, AbstractTablesTest.class); + owner = SSLUtil.createSubject(cert); + log.debug("created owner: " + owner); + + cert = FileUtil.getFileFromResource(MEMBER_CERT, AbstractTablesTest.class); + member = SSLUtil.createSubject(cert); + log.debug("created member: " + member); + + } catch (Exception ex) { + log.error("setup failed", ex); + throw new IllegalStateException("failed to create DataSource", ex); + } + } + + @Test + public void testTemplateDAO() { + try { + String testName = "test-template"; + String testTemplate = getTestTemplate("caomPublisherID"); + + ServiceDescriptorTemplate expected = new ServiceDescriptorTemplate(testName, testTemplate); + expected.owner = owner; + expected.ownerID = AuthenticationUtil.getIdentityManager().toOwner(owner); + log.debug("expected: " + expected); + + // PUT a descriptor + TemplateDAO templateDAO = new TemplateDAO(dataSource); + templateDAO.put(expected); + + // GET the descriptor + ServiceDescriptorTemplate actual = templateDAO.get(owner, testName); + validate(expected, actual, true); + + // UPDATE the descriptor + expected.owner = member; + expected.ownerID = AuthenticationUtil.getIdentityManager().toOwner(member); + templateDAO.put(expected); + + // GET the descriptor again to verify update + actual = templateDAO.get(member, testName); + Assert.assertNotNull("expected null", actual); + validate(expected, actual, true); + + // DELETE the descriptor + templateDAO.delete(member, testName); + + // GET the descriptor again to verify deletion + actual = templateDAO.get(member, testName); + Assert.assertNull("expected null", actual); + + } catch (Exception unexpected) { + log.error("unexpected exception", unexpected); + Assert.fail("unexpected exception: " + unexpected); + } + } + + private void validate(ServiceDescriptorTemplate expected, ServiceDescriptorTemplate actual, boolean checkOwner) { + Assert.assertEquals(expected.getName(), actual.getName()); + Assert.assertEquals(expected.getTemplate(), actual.getTemplate()); + if (checkOwner) { + Assert.assertEquals(expected.owner, actual.owner); + Assert.assertEquals(expected.ownerID, actual.ownerID); + } + Assert.assertEquals(expected.getIdentifiers(), actual.getIdentifiers()); + Assert.assertNotNull(actual.getResource()); + VOTableResource expectedResource = expected.getResource(); + VOTableResource actualResource = actual.getResource(); + Assert.assertEquals(expectedResource.getType(), actualResource.getType()); + Assert.assertEquals(expectedResource.utype, actualResource.utype); + Assert.assertFalse(actual.getResource().getGroups().isEmpty()); + VOTableGroup expectedGroup = expectedResource.getGroups().get(0); + VOTableGroup actualGroup = actualResource.getGroups().get(0); + Assert.assertEquals(expectedGroup.getName(), actualGroup.getName()); + Assert.assertEquals(expectedGroup.getParams().size(), actualGroup.getParams().size()); + Assert.assertEquals(expectedGroup.getParams().get(0).getName(), actualGroup.getParams().get(0).getName()); + } + + private String getTestTemplate(String identifier) throws IOException { + File testFile = FileUtil.getFileFromResource("valid-template.xml", TemplateDAOTest.class); + String template = Files.readString(testFile.toPath()); + return template.replace("IDENTIFIER", identifier); + } + +} diff --git a/youcat/src/intTest/resources/valid-template.xml b/youcat/src/intTest/resources/valid-template.xml new file mode 100644 index 00000000..f34d091a --- /dev/null +++ b/youcat/src/intTest/resources/valid-template.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/youcat/src/main/java/org/opencadc/youcat/InitDatabaseSD.java b/youcat/src/main/java/org/opencadc/youcat/InitDatabaseSD.java new file mode 100644 index 00000000..8e35cab0 --- /dev/null +++ b/youcat/src/main/java/org/opencadc/youcat/InitDatabaseSD.java @@ -0,0 +1,100 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2025. (c) 2025. + * 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 : + * . + * + * : 5 $ + * + ************************************************************************ + */ + +package org.opencadc.youcat; + +import ca.nrc.cadc.db.version.InitDatabase; +import java.net.URL; +import java.util.Collections; +import javax.sql.DataSource; + +public class InitDatabaseSD extends InitDatabase { + + public static final String MODEL_NAME = "DESCRIPTORS"; // must be <= 16 chars + public static final String MODEL_VERSION = "1.0.0"; + public static final String PREV_MODEL_VERSION = "1.0.0"; + + static String[] CREATE_SQL = new String[] { + "tap_schema.ServiceDescriptorTemplate.sql" + }; + + static String[] UPGRADE_SQL = new String[]{}; + + public InitDatabaseSD(DataSource dataSource, String database, String schema) { + super(dataSource, database, schema, MODEL_NAME, MODEL_VERSION, PREV_MODEL_VERSION); + Collections.addAll(createSQL, CREATE_SQL); + Collections.addAll(upgradeSQL, UPGRADE_SQL); + } + + @Override + protected URL findSQL(String fname) { + return InitDatabaseSD.class.getClassLoader().getResource("postgresql/" + fname); + } + +} diff --git a/youcat/src/main/java/org/opencadc/youcat/TemplateDAO.java b/youcat/src/main/java/org/opencadc/youcat/TemplateDAO.java index 7a768b5b..2d9d6379 100644 --- a/youcat/src/main/java/org/opencadc/youcat/TemplateDAO.java +++ b/youcat/src/main/java/org/opencadc/youcat/TemplateDAO.java @@ -69,23 +69,37 @@ package org.opencadc.youcat; +import ca.nrc.cadc.auth.AuthenticationUtil; +import ca.nrc.cadc.auth.IdentityManager; +import ca.nrc.cadc.db.version.KeyValue; +import ca.nrc.cadc.db.version.KeyValueDAO; import ca.nrc.cadc.tap.schema.AbstractDAO; +import java.security.Principal; +import java.util.ArrayList; import java.util.List; +import java.util.Set; import javax.security.auth.Subject; import javax.sql.DataSource; +import org.apache.log4j.Logger; import org.opencadc.datalink.ServiceDescriptorTemplate; /** * DAO for the ServiceDescriptor table. */ public class TemplateDAO extends AbstractDAO { + private static final Logger log = Logger.getLogger(TemplateDAO.class); + + private final KeyValueDAO keyValueDAO; /** * Create a TemplateDAO with a TapSchemaDAO, to use the same DataSource. * * @param abstractDAO a TapSchemaDAO */ - public TemplateDAO(AbstractDAO abstractDAO) {} + public TemplateDAO(AbstractDAO abstractDAO) { + super(abstractDAO); + this.keyValueDAO = new KeyValueDAO(dataSource, null, "tap_schema", ServiceDescriptorTemplate.class); + } /** * Create a TemplateDAO using a DataSource. Useful @@ -93,7 +107,10 @@ public TemplateDAO(AbstractDAO abstractDAO) {} * * @param dataSource a datasource */ - TemplateDAO(DataSource dataSource) {} + TemplateDAO(DataSource dataSource) { + super(dataSource); + this.keyValueDAO = new KeyValueDAO(dataSource, null, "tap_schema", ServiceDescriptorTemplate.class); + } /** * Get the template with the given name. @@ -104,7 +121,16 @@ public TemplateDAO(AbstractDAO abstractDAO) {} * @throws org.springframework.dao.DataAccessException if there is a problem querying the database. */ public ServiceDescriptorTemplate get(Subject owner, String name) { - throw new UnsupportedOperationException("Not supported yet."); + Object ownerID = getOwnerID(owner); + String key = generateKey(name, ownerID); + KeyValue keyValue = keyValueDAO.get(key); + if (keyValue == null) { + return null; + } + ServiceDescriptorTemplate template = new ServiceDescriptorTemplate(name, keyValue.value); + template.owner = owner; + template.ownerID = ownerID; + return template; } /** @@ -114,7 +140,13 @@ public ServiceDescriptorTemplate get(Subject owner, String name) { * @throws org.springframework.dao.DataAccessException if there is a problem inserting into the database. */ public void put(ServiceDescriptorTemplate template) { - throw new UnsupportedOperationException("Not supported yet."); + String key = generateKey(template.getName(), template.ownerID); + KeyValue keyValue = keyValueDAO.get(key); + if (keyValue == null) { + keyValue = new KeyValue(key); + } + keyValue.value = template.getTemplate(); + keyValueDAO.put(keyValue); } /** @@ -125,7 +157,8 @@ public void put(ServiceDescriptorTemplate template) { * @throws org.springframework.dao.DataAccessException if there is a problem deleting from the database. */ public void delete(Subject owner, String name) { - throw new UnsupportedOperationException("Not supported yet."); + String key = generateKey(name, getOwnerID(owner)); + keyValueDAO.delete(key); } /** @@ -139,7 +172,18 @@ public void delete(Subject owner, String name) { * @throws org.springframework.dao.DataAccessException if there is a problem querying the database. */ public List list(List identifiers) { - throw new UnsupportedOperationException("Not supported yet."); + List templates = new ArrayList<>(); + List keyValues = keyValueDAO.list(); + for (KeyValue keyValue : keyValues) { + ServiceDescriptorTemplate template = new ServiceDescriptorTemplate(keyValue.getName(), keyValue.value); + if (template.getIdentifiers().stream().anyMatch(identifiers::contains) + && !templates.contains(template)) { + template.ownerID = keyValue.getName().substring(keyValue.getName().indexOf(':') + 1); + template.owner = AuthenticationUtil.getIdentityManager().toSubject(template.ownerID); + templates.add(template); + } + } + return templates; } /** @@ -151,7 +195,56 @@ public List list(List identifiers) { * @return a list of ServiceDescriptorTemplate's */ public List list(Subject owner) { - throw new UnsupportedOperationException("Not supported yet."); + IdentityManager identityManager = AuthenticationUtil.getIdentityManager(); + List templates = new ArrayList<>(); + List keyValues = keyValueDAO.list(); + for (KeyValue keyValue : keyValues) { + String templateOwnerID = keyValue.getName().substring(keyValue.getName().indexOf(':') + 1); + Subject templateSubject = identityManager.toSubject(templateOwnerID); + // Compare subject principals + if (isOwner(owner, templateSubject)) { + ServiceDescriptorTemplate template = new ServiceDescriptorTemplate(keyValue.getName(), keyValue.value); + template.owner = owner; + template.ownerID = templateOwnerID; + templates.add(template); + } + } + return templates; + } + + private Object getOwnerID(Subject owner) { + IdentityManager identityManager = AuthenticationUtil.getIdentityManager(); + return identityManager.toOwner(owner); + } + + // generate the key for the KeyValue pair + private String generateKey(String name, Object ownerID) { + if (name == null || ownerID == null) { + throw new IllegalArgumentException("name or ownerID cannot be null"); + } + return name + ":" + ownerID2String(ownerID); + } + + private String ownerID2String(Object ownerID) { + if (ownerID instanceof String) { + return (String) ownerID; + } else { + return ownerID.toString(); + } + } + + // check if an owner principal matches an templateSubject principal + private boolean isOwner(Subject owner, Subject templateSubject) { + Set ownerPrincipals = owner.getPrincipals(); + Set templatePrincipals = templateSubject.getPrincipals(); + for (Principal ownerPrincipal : ownerPrincipals) { + for (Principal templatePrincipal : templatePrincipals) { + if (AuthenticationUtil.equals(ownerPrincipal, templatePrincipal)) { + return true; + } + } + } + return false; } } diff --git a/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java b/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java index 22a2cbbd..66245048 100644 --- a/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java +++ b/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java @@ -92,15 +92,12 @@ public class YoucatInitAction extends InitAction { private static final String YOUCAT_ADMIN = YOUCAT + ".adminUser"; private static final String YOUCAT_CREATE = YOUCAT + ".createSchemaInDB"; - private String jndiAdminKey; - private String jndiCreateSchemaKey; - - public YoucatInitAction() { + public YoucatInitAction() { } private void initConfig() { - this.jndiAdminKey = appName + TablesAction.ADMIN_KEY; - this.jndiCreateSchemaKey = appName + TablesAction.CREATE_SCHEMA_KEY; + String jndiAdminKey = appName + TablesAction.ADMIN_KEY; + String jndiCreateSchemaKey = appName + TablesAction.CREATE_SCHEMA_KEY; PropertiesReader r = new PropertiesReader("youcat.properties"); MultiValuedProperties mvp = r.getAllProperties(); @@ -110,7 +107,7 @@ private void initConfig() { boolean ok = true; String adminUser = mvp.getFirstPropertyValue(YOUCAT_ADMIN); - sb.append("\n\t" + YOUCAT_ADMIN + ": "); + sb.append("\n\t").append(YOUCAT_ADMIN).append(": "); if (adminUser == null) { sb.append("MISSING"); } else { @@ -118,7 +115,7 @@ private void initConfig() { } String yc = mvp.getFirstPropertyValue(YOUCAT_CREATE); - sb.append("\n\t" + YOUCAT_CREATE + ": "); + sb.append("\n\t").append(YOUCAT_CREATE).append(": "); if (yc == null) { sb.append("MISSING"); } else { @@ -129,18 +126,13 @@ private void initConfig() { throw new InvalidConfigException(sb.toString()); } - Boolean createSchemaInDB = false; // default: false for backwards compat - if (yc != null && "true".equals(yc)) { - createSchemaInDB = true; - } + boolean createSchemaInDB = "true".equals(yc); // default: false for backwards compat try { Context ctx = new InitialContext(); if (adminUser != null) { ctx.bind(jndiAdminKey, new HttpPrincipal(adminUser)); } - if (createSchemaInDB != null) { - ctx.bind(jndiCreateSchemaKey, createSchemaInDB); - } + ctx.bind(jndiCreateSchemaKey, createSchemaInDB); log.info("init: admin=" + adminUser + " createSchemaInDB=" + createSchemaInDB); } catch (Exception ex) { log.error("Failed to create JNDI key(s): " + jndiAdminKey + "|" + jndiCreateSchemaKey, ex); @@ -165,8 +157,12 @@ public void doInit() { InitDatabaseUWS uwsi = new InitDatabaseUWS(uws, null, "uws"); uwsi.doInit(); log.info("InitDatabaseUWS: OK"); - - + + // ServiceDescriptors + log.info("InitDatabaseYoucat: START"); + InitDatabaseSD youcat = new InitDatabaseSD(tapadm, null, "tap_schema"); + youcat.doInit(); + log.info("InitDatabaseYoucat: OK"); } catch (Exception ex) { throw new RuntimeException("INIT FAIL: " + ex.getMessage(), ex); } diff --git a/youcat/src/main/resources/postgresql/tap_schema.ServiceDescriptorTemplate.sql b/youcat/src/main/resources/postgresql/tap_schema.ServiceDescriptorTemplate.sql new file mode 100644 index 00000000..5eccddd2 --- /dev/null +++ b/youcat/src/main/resources/postgresql/tap_schema.ServiceDescriptorTemplate.sql @@ -0,0 +1,8 @@ + +create table tap_schema.ServiceDescriptorTemplate +( + name varchar(128) not null primary key, + value text not null, + lastModified timestamp not null +) +;