From 8511e59702da225cc104a91a0bebbe0cbfafddc0 Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Wed, 17 Jul 2024 09:52:30 -0700 Subject: [PATCH 01/11] cadc-tap-schema: add support for admin to create schema --- cadc-tap-schema/build.gradle | 2 +- .../ca/nrc/cadc/vosi/actions/GetAction.java | 33 +++- .../vosi/actions/GetPermissionsAction.java | 26 ++-- .../vosi/actions/PostPermissionsAction.java | 13 +- .../ca/nrc/cadc/vosi/actions/PutAction.java | 143 +++++++++++++++-- .../cadc/vosi/actions/TableDescHandler.java | 40 ++++- .../nrc/cadc/vosi/actions/TablesAction.java | 145 +++++++++++++----- .../java/ca/nrc/cadc/vosi/actions/Util.java | 40 ++--- 8 files changed, 333 insertions(+), 109 deletions(-) diff --git a/cadc-tap-schema/build.gradle b/cadc-tap-schema/build.gradle index aa58b776..d290c8b8 100644 --- a/cadc-tap-schema/build.gradle +++ b/cadc-tap-schema/build.gradle @@ -16,7 +16,7 @@ sourceCompatibility = 1.8 group = 'org.opencadc' -version = '1.1.32' +version = '1.1.33' description = 'OpenCADC TAP-1.1 tap schema server library' def git_url = 'https://github.com/opencadc/tap' diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetAction.java index e29307d3..62ce245a 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetAction.java @@ -69,6 +69,7 @@ import ca.nrc.cadc.net.ResourceNotFoundException; import ca.nrc.cadc.rest.RestAction; +import ca.nrc.cadc.tap.schema.SchemaDesc; import ca.nrc.cadc.tap.schema.TableDesc; import ca.nrc.cadc.tap.schema.TapSchema; import ca.nrc.cadc.tap.schema.TapSchemaDAO; @@ -93,8 +94,14 @@ public GetAction() { @Override public void doAction() throws Exception { - String tableName = getTableName(); - log.debug("GET: " + tableName); + String schemaName = null; + String tableName = null; + String[] target = getTarget(); + if (target != null) { + schemaName = target[0]; + tableName = target[1]; + } + log.debug("GET: " + schemaName + " " + tableName); if (!readable) { throw new AccessControlException(RestAction.STATE_OFFLINE_MSG); @@ -118,13 +125,35 @@ public void doAction() throws Exception { if (tableName != null) { checkTableReadPermissions(dao, tableName); TableDesc td = dao.getTable(tableName); + if (td == null) { + // currently, permission check already threw this + throw new ResourceNotFoundException("table not found: " + tableName); + } TableWriter tw = new TableWriter(); syncOutput.setCode(HttpServletResponse.SC_OK); syncOutput.setHeader("Content-Type", "text/xml"); tw.write(td, new OutputStreamWriter(syncOutput.getOutputStream())); + } else if (schemaName != null) { + checkViewSchemaPermissions(dao, schemaName); + // TODO: TapSchemaDAO only supports schema only, ok for detail=min + // should at least list tables for default detail + // should provide columns at detail=max + SchemaDesc sd = dao.getSchema(schemaName, (depth == TapSchemaDAO.MIN_DEPTH)); + if (sd == null) { + // currently, permission check already threw this + throw new ResourceNotFoundException("schema not found: " + schemaName); + } + TapSchema tapSchema = new TapSchema(); + tapSchema.getSchemaDescs().add(sd); + + TableSetWriter tsw = new TableSetWriter(); + syncOutput.setCode(HttpServletResponse.SC_OK); + syncOutput.setHeader("Content-Type", "text/xml"); + tsw.write(tapSchema, new OutputStreamWriter(syncOutput.getOutputStream())); } else { TapSchemaLoader loader = new TapSchemaLoader(dao); TapSchema tapSchema = loader.load(depth); + TableSetWriter tsw = new TableSetWriter(); syncOutput.setCode(HttpServletResponse.SC_OK); syncOutput.setHeader("Content-Type", "text/xml"); diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetPermissionsAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetPermissionsAction.java index 8f4c576f..ba689ac2 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetPermissionsAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetPermissionsAction.java @@ -67,20 +67,15 @@ package ca.nrc.cadc.vosi.actions; +import ca.nrc.cadc.rest.InlineContentHandler; +import ca.nrc.cadc.tap.schema.TapPermissions; +import ca.nrc.cadc.tap.schema.TapSchemaDAO; import java.io.OutputStream; - import javax.security.auth.Subject; import javax.security.auth.x500.X500Principal; - import org.apache.log4j.Logger; import org.opencadc.gms.GroupURI; -import ca.nrc.cadc.auth.HttpPrincipal; -import ca.nrc.cadc.auth.NumericPrincipal; -import ca.nrc.cadc.rest.InlineContentHandler; -import ca.nrc.cadc.tap.schema.TapPermissions; -import ca.nrc.cadc.tap.schema.TapSchemaDAO; - /** * Return the permissions for the object identified by the 'name' * parameter. @@ -94,15 +89,18 @@ public class GetPermissionsAction extends TablesAction { @Override public void doAction() throws Exception { - String name = getTableName(); - log.debug("POST: " + name); + String[] target = getTarget(); + if (target == null) { + throw new IllegalArgumentException("no schema|table name in path"); + } + String name = target[0]; // schema + if (target[1] != null) { + name = target[1]; // table + } + log.debug("name: " + name); checkWritable(); - if (name == null) { - throw new IllegalArgumentException( "Missing param: name"); - } - TapSchemaDAO dao = getTapSchemaDAO(); TapPermissions permissions = null; if (Util.isSchemaName(name)) { diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PostPermissionsAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PostPermissionsAction.java index 4c823dac..6fd0a45a 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PostPermissionsAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PostPermissionsAction.java @@ -101,12 +101,15 @@ public PostPermissionsAction() { @Override public void doAction() throws Exception { - - String name = getTableName(); - log.debug("name: " + name); - if (name == null) { - throw new IllegalArgumentException("Missing param: name"); + String[] target = getTarget(); + if (target == null) { + throw new IllegalArgumentException("no schema|table name in path"); } + String name = target[0]; // schema + if (target[1] != null) { + name = target[1]; // table + } + log.debug("name: " + name); checkWritable(); diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PutAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PutAction.java index 827d8b3c..2c06ee9f 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PutAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PutAction.java @@ -68,19 +68,22 @@ package ca.nrc.cadc.vosi.actions; import ca.nrc.cadc.auth.AuthenticationUtil; +import ca.nrc.cadc.auth.HttpPrincipal; +import ca.nrc.cadc.auth.IdentityManager; import ca.nrc.cadc.db.DatabaseTransactionManager; import ca.nrc.cadc.net.ResourceAlreadyExistsException; import ca.nrc.cadc.profiler.Profiler; import ca.nrc.cadc.rest.InlineContentHandler; -import ca.nrc.cadc.rest.RestAction; import ca.nrc.cadc.tap.db.TableCreator; import ca.nrc.cadc.tap.schema.ColumnDesc; +import ca.nrc.cadc.tap.schema.SchemaDesc; import ca.nrc.cadc.tap.schema.TableDesc; import ca.nrc.cadc.tap.schema.TapPermissions; import ca.nrc.cadc.tap.schema.TapSchemaDAO; -import java.security.AccessControlException; +import javax.security.auth.Subject; import javax.sql.DataSource; import org.apache.log4j.Logger; +import org.springframework.jdbc.core.JdbcTemplate; /** * Create table. This action creates a new database table and adds a description @@ -98,16 +101,95 @@ public PutAction() { @Override public void doAction() throws Exception { - String tableName = getTableName(); - String schemaName = Util.getSchemaFromTable(tableName); - log.debug("PUT: " + tableName); + String[] target = getTarget(); + String schemaName = target[0]; + String tableName = target[1]; + + log.debug("PUT: schema=" + schemaName + " table=" + tableName); checkWritable(); TapSchemaDAO ts = getTapSchemaDAO(); - checkSchemaWritePermissions(ts, schemaName); + if (tableName == null) { + // create schema + checkIsAdmin(); + } else { + // create table + checkSchemaWritePermissions(ts, schemaName); + } + if (tableName != null) { + createTable(ts, schemaName, tableName); + } else if (schemaName != null) { + createSchema(ts, schemaName); + } + } + + private void createSchema(TapSchemaDAO ts, String schema) throws Exception { + log.warn("createSchema: " + schema + " START"); + String owner = getInputSchemaOwner(); + Subject s = new Subject(); + s.getPrincipals().add(new HttpPrincipal(owner)); + + SchemaDesc sd = new SchemaDesc(schema); + TapPermissions perms = new TapPermissions(); + perms.owner = AuthenticationUtil.getIdentityManager().augment(s); + String[] createSQL = new String[] { + "CREATE SCHEMA " + schema, + "grant usage on schema " + schema + " to public", + "grant select on all tables in schema " + schema + " to public" + }; + + DataSource ds = getDataSource(); + ts.setDataSource(ds); + DatabaseTransactionManager tm = new DatabaseTransactionManager(ds); + try { + tm.startTransaction(); + + if (getCreateSchemaEnabled()) { + JdbcTemplate jdbc = new JdbcTemplate(ds); + for (String sql : createSQL) { + log.warn(sql); + jdbc.execute(sql); + } + } + log.warn("update tap_schema: " + sd); + ts.put(sd); + log.warn("set permissions: " + perms); + ts.setSchemaPermissions(schema, perms); + + tm.commitTransaction(); + } catch (Exception ex) { + try { + log.error("PUT failed - rollback", ex); + tm.rollbackTransaction(); + log.error("PUT failed - rollback: OK"); + } catch (Exception oops) { + log.error("PUT failed - rollback : FAIL", oops); + } + log.warn("createSchema: " + schema + " FAIL"); + // TODO: categorise failures better + throw new RuntimeException("failed to create schema " + schema, ex); + } finally { + + if (tm.isOpen()) { + log.error("BUG: open transaction in finally - trying to rollback"); + try { + tm.rollbackTransaction(); + log.error("BUG: rollback in finally: OK"); + } catch (Exception oops) { + log.error("BUG: rollback in finally: FAIL", oops); + } + log.warn("createSchema: " + schema + " FAIL"); + throw new RuntimeException("BUG: open transaction in finally"); + } + log.warn("createSchema: " + schema + " DONE"); + } + + } + + private void createTable(TapSchemaDAO ts, String schemaName, String tableName) throws Exception { TableDesc inputTable = getInputTable(schemaName, tableName); if (inputTable == null) { throw new IllegalArgumentException("no input table"); @@ -191,20 +273,47 @@ protected InlineContentHandler getInlineContentHandler() { return new TableDescHandler(INPUT_TAG); } + private String getInputSchemaOwner() { + Object in = syncInput.getContent(INPUT_TAG); + if (in == null) { + throw new IllegalArgumentException("no input: expected a document describing the schema owner for create"); + } + if (in instanceof String) { + String schemaOwner = (String) in; + return schemaOwner; + } + throw new RuntimeException("BUG: no input schema owner"); + } + + // unused because VOSO tableset schema does not include owner + private SchemaDesc getInputSchema(String schemaName) { + Object in = syncInput.getContent(INPUT_TAG); + if (in == null) { + throw new IllegalArgumentException("no input: expected a document describing the schema to create"); + } + if (in instanceof SchemaDesc) { + SchemaDesc input = (SchemaDesc) in; + return input; + } + throw new RuntimeException("BUG: no input schema"); + } + private TableDesc getInputTable(String schemaName, String tableName) { - TableDesc input = (TableDesc) syncInput.getContent(INPUT_TAG); - if (input == null) { + Object in = syncInput.getContent(INPUT_TAG); + if (in == null) { throw new IllegalArgumentException("no input: expected a document describing the table to create"); } - - input.setSchemaName(schemaName); - input.setTableName(tableName); - int c = 0; - for (ColumnDesc cd : input.getColumnDescs()) { - cd.setTableName(tableName); - cd.column_index = c++; + if (in instanceof TableDesc) { + TableDesc input = (TableDesc) in; + input.setSchemaName(schemaName); + input.setTableName(tableName); + int c = 0; + for (ColumnDesc cd : input.getColumnDescs()) { + cd.setTableName(tableName); + cd.column_index = c++; + } + return input; } - - return input; + throw new RuntimeException("BUG: no input table"); } } diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TableDescHandler.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TableDescHandler.java index 74a88d4a..5800ed59 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TableDescHandler.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TableDescHandler.java @@ -69,25 +69,24 @@ import ca.nrc.cadc.dali.tables.votable.VOTableDocument; -import ca.nrc.cadc.dali.tables.votable.VOTableField; import ca.nrc.cadc.dali.tables.votable.VOTableReader; import ca.nrc.cadc.dali.tables.votable.VOTableResource; import ca.nrc.cadc.dali.tables.votable.VOTableTable; import ca.nrc.cadc.io.ByteCountInputStream; import ca.nrc.cadc.rest.InlineContentException; import ca.nrc.cadc.rest.InlineContentHandler; -import ca.nrc.cadc.tap.schema.ColumnDesc; +import ca.nrc.cadc.tap.schema.SchemaDesc; import ca.nrc.cadc.tap.schema.TableDesc; -import ca.nrc.cadc.tap.schema.TapDataType; +import ca.nrc.cadc.tap.schema.TapSchema; import ca.nrc.cadc.tap.schema.TapSchemaUtil; import ca.nrc.cadc.util.StringUtil; import ca.nrc.cadc.vosi.InvalidTableSetException; import ca.nrc.cadc.vosi.TableReader; +import ca.nrc.cadc.vosi.TableSetReader; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; -import java.util.ArrayList; -import java.util.List; +import java.util.Properties; import org.apache.log4j.Logger; /** @@ -100,6 +99,9 @@ public class TableDescHandler implements InlineContentHandler { private static final long BYTE_LIMIT = 1024 * 1024L; // 1 MiB public static final String VOSI_TABLE_TYPE = "text/xml"; public static final String VOTABLE_TYPE = "application/x-votable+xml"; + //public static final String VOSI_SCHEMA_TYPE = "text/xml;content=schema"; + // VOSI tableset schema cannot carry owner information + public static final String VOSI_SCHEMA_TYPE = "text/plain"; // key = value private String objectTag; @@ -110,12 +112,31 @@ public TableDescHandler(String objectTag) { @Override public Content accept(String name, String contentType, InputStream in) throws InlineContentException, IOException { try { + String schemaOwner = null; + SchemaDesc sch = null; TableDesc tab = null; - if (VOSI_TABLE_TYPE.equalsIgnoreCase(contentType)) { - TableReader tr = new TableReader(false); // schema validation causes default arraysize="1" to be injected + if (VOSI_SCHEMA_TYPE.equalsIgnoreCase(contentType)) { + ByteCountInputStream istream = new ByteCountInputStream(in, BYTE_LIMIT); + String str = StringUtil.readFromInputStream(istream, "UTF-8"); + log.debug("raw input:\n" + str); + + //TableSetReader tsr = new TableSetReader(false); // schema validation causes default arraysize="1" to be injected + //TapSchema ts = tsr.read(new StringReader(str)); + //if (ts.getSchemaDescs().isEmpty() || ts.getSchemaDescs().size() > 1) { + // throw new IllegalArgumentException("invalid input: expected 1 schema in " + VOSI_SCHEMA_TYPE + // + " found: " + ts.getSchemaDescs().size()); + //} + // TODO: reject if there are tables in schema + //sch = ts.getSchemaDescs().get(0); + Properties props = new Properties(); + props.load(new StringReader(str)); + schemaOwner = props.getProperty("owner"); + } else if (VOSI_TABLE_TYPE.equalsIgnoreCase(contentType)) { ByteCountInputStream istream = new ByteCountInputStream(in, BYTE_LIMIT); String xml = StringUtil.readFromInputStream(istream, "UTF-8"); - log.debug("input xml:\n" + xml); + + TableReader tr = new TableReader(false); // schema validation causes default arraysize="1" to be injected + log.debug("raw input:\n" + xml); tab = tr.read(new StringReader(xml)); } else if (VOTABLE_TYPE.equalsIgnoreCase(contentType)) { VOTableReader tr = new VOTableReader(); @@ -126,6 +147,9 @@ public Content accept(String name, String contentType, InputStream in) throws In InlineContentHandler.Content ret = new InlineContentHandler.Content(); ret.name = objectTag; ret.value = tab; + if (schemaOwner != null) { + ret.value = schemaOwner; + } return ret; } catch (InvalidTableSetException ex) { throw new IllegalArgumentException("invalid input document", ex); diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TablesAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TablesAction.java index 50d61d03..bca65cf4 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TablesAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TablesAction.java @@ -68,28 +68,29 @@ package ca.nrc.cadc.vosi.actions; import ca.nrc.cadc.auth.AuthenticationUtil; +import ca.nrc.cadc.auth.HttpPrincipal; import ca.nrc.cadc.log.WebServiceLogInfo; import ca.nrc.cadc.net.ResourceNotFoundException; import ca.nrc.cadc.net.TransientException; -import ca.nrc.cadc.reg.Standards; -import ca.nrc.cadc.reg.client.LocalAuthority; import ca.nrc.cadc.rest.InlineContentHandler; import ca.nrc.cadc.rest.RestAction; import ca.nrc.cadc.tap.PluginFactory; import ca.nrc.cadc.tap.schema.TapPermissions; import ca.nrc.cadc.tap.schema.TapSchemaDAO; -import java.net.URI; +import java.io.IOException; import java.security.AccessControlException; -import java.security.Principal; -import java.util.ArrayList; -import java.util.List; +import java.security.Principal; +import java.util.Set; +import java.util.TreeSet; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; import javax.security.auth.Subject; import javax.sql.DataSource; import org.apache.log4j.Logger; -import org.opencadc.gms.GroupClient; import org.opencadc.gms.GroupURI; -import org.opencadc.gms.GroupUtil; +import org.opencadc.gms.IvoaGroupClient; /** * @@ -98,16 +99,29 @@ public abstract class TablesAction extends RestAction { private static final Logger log = Logger.getLogger(TablesAction.class); + public static String ADMIN_KEY = "-admin-principal"; + public static String CREATE_SCHEMA_KEY = "-create-schema-in-db"; + protected static final String PERMS_CONTENTTYPE = "text/plain"; protected static final String OWNER_KEY = "owner"; protected static final String PUBLIC_KEY = "public"; protected static final String RGROUP_KEY = "r-group"; protected static final String RWGROUP_KEY = "rw-group"; + protected String jndiAdminKey; + protected String jndiCreateSchemaKey; + public TablesAction() { super(); } + @Override + public void initAction() throws Exception { + super.initAction(); + this.jndiAdminKey = appName + TablesAction.ADMIN_KEY; + this.jndiCreateSchemaKey = appName + TablesAction.CREATE_SCHEMA_KEY; + } + protected final DataSource getDataSource() { PluginFactory pf = new PluginFactory(); DataSourceProvider dsf = pf.getDataSourceProvider(); @@ -125,13 +139,31 @@ protected InlineContentHandler getInlineContentHandler() { return null; } - String getTableName() { + String getTableName() throws ResourceNotFoundException { + String[] ss = getTarget(); + if (ss == null) { + return null; + } + return ss[1]; + } + + // return {schema} and {table} + String[] getTarget() throws ResourceNotFoundException { String path = syncInput.getPath(); // TODO: move this empty str to null up to SyncInput? - if (path != null && path.isEmpty()) { + if (path == null || path.isEmpty()) { return null; } - return path; + String[] st = path.split("[.]"); + if (st.length > 2) { + throw new ResourceNotFoundException("not found: " + path + " (reason: invalid schema|table name -- too many dots)"); + } + String[] ret = new String[2]; + ret[0] = st[0]; + if (st.length == 2) { + ret[1] = path; + } + return ret; } /** @@ -182,10 +214,22 @@ TapPermissions checkViewSchemaPermissions(TapSchemaDAO dao, String schemaName) if (schemaPermissions == null) { throw new ResourceNotFoundException("schema not found: " + schemaName); } + if (schemaPermissions.owner == null) { + super.logInfo.setMessage("view table allowed: null schema owner"); + return schemaPermissions; + } + if (schemaPermissions.isPublic) { + super.logInfo.setMessage("view table allowed: public schema"); + return schemaPermissions; + } if (Util.isOwner(schemaPermissions)) { super.logInfo.setMessage("view schema permissions allowed: schema owner"); return schemaPermissions; } + if (checkIsAdmin()) { + super.logInfo.setMessage("view schema permissions allowed: admin"); + return schemaPermissions; + } throw new AccessControlException("permission denied"); } @@ -210,10 +254,11 @@ TapPermissions checkViewTablePermissions(TapSchemaDAO dao, String tableName) String schemaName = Util.getSchemaFromTable(tableName); TapPermissions schemaPermissions = dao.getSchemaPermissions(schemaName); - TapPermissions tablePermissions = dao.getTablePermissions(tableName); if (schemaPermissions == null) { throw new ResourceNotFoundException("schema not found: " + schemaName); } + + TapPermissions tablePermissions = dao.getTablePermissions(tableName); if (tablePermissions == null) { throw new ResourceNotFoundException("table not found: " + tableName); } @@ -225,6 +270,10 @@ TapPermissions checkViewTablePermissions(TapSchemaDAO dao, String tableName) super.logInfo.setMessage("view table permissions allowed: table owner"); return tablePermissions; } + if (checkIsAdmin()) { + super.logInfo.setMessage("view table permissions allowed: admin"); + return tablePermissions; + } throw new AccessControlException("permission denied"); } @@ -255,7 +304,7 @@ void checkModifyTablePermissionsPermissions(TapSchemaDAO dao, String tableName) // if anon or authenticated check public // if authenticated check schema and table owners, readGroups, readWriteGroups void checkTableReadPermissions(TapSchemaDAO dao, String tableName) - throws AccessControlException, ResourceNotFoundException { + throws AccessControlException, IOException, InterruptedException, ResourceNotFoundException { TapPermissions tablePermissions = dao.getTablePermissions(tableName); if (tablePermissions == null) { @@ -294,15 +343,16 @@ void checkTableReadPermissions(TapSchemaDAO dao, String tableName) super.logInfo.setMessage("view table allowed: schema owner"); return; } + if (checkIsAdmin()) { + super.logInfo.setMessage("view table allowed: admin"); + return; + } // check group permissions // The serviceID should come from the read or readWrite group // in the future - LocalAuthority localAuthority = new LocalAuthority(); - URI serviceURI = localAuthority.getServiceURI(Standards.GMS_SEARCH_01.toString()); - GroupClient groupClient = GroupUtil.getGroupClient(serviceURI); - - List readGroups = new ArrayList(4); + final IvoaGroupClient groupClient = new IvoaGroupClient(); + Set readGroups = new TreeSet<>(); if (schemaPermissions.readGroup != null) { readGroups.add(schemaPermissions.readGroup); } @@ -321,19 +371,12 @@ void checkTableReadPermissions(TapSchemaDAO dao, String tableName) super.logInfo.setMessage("view table allowed: member of group " + permittingGroup); return; } - -// GroupURI readGroup = Util.getReadPermissionsGroup(groupClient, schemaPermissions); -// -// readGroup = Util.getReadPermissionsGroup(groupClient, tablePermissions); -// if (readGroup != null) { -// super.logInfo.setMessage("view table allowed: member of table group " + readGroup); -// return; -// } + throw new AccessControlException("permission denied"); } public void checkTableWritePermissions(TapSchemaDAO dao, String tableName) - throws AccessControlException, ResourceNotFoundException { + throws AccessControlException, IOException, ResourceNotFoundException { TablesAction.checkTableWritePermissions(dao, tableName, logInfo); } @@ -341,7 +384,7 @@ public void checkTableWritePermissions(TapSchemaDAO dao, String tableName) // if authenticated table owners, readWriteGroup members // static method here so that TableUpdateRunner can make this call static void checkTableWritePermissions(TapSchemaDAO dao, String tableName, WebServiceLogInfo logInfo) - throws AccessControlException, ResourceNotFoundException { + throws AccessControlException, IOException, ResourceNotFoundException { TapPermissions tablePermissions = dao.getTablePermissions(tableName); if (tablePermissions == null) { @@ -351,11 +394,8 @@ static void checkTableWritePermissions(TapSchemaDAO dao, String tableName, WebSe logInfo.setMessage("table write allowed: table owner"); return; } - // The serviceID should come from the readWrite group in the future - LocalAuthority localAuthority = new LocalAuthority(); - URI serviceURI = localAuthority.getServiceURI(Standards.GMS_SEARCH_01.toString()); - GroupClient groupClient = GroupUtil.getGroupClient(serviceURI); - List permittedGroups = new ArrayList(1); + final IvoaGroupClient groupClient = new IvoaGroupClient(); + Set permittedGroups = new TreeSet<>(); if (tablePermissions.readWriteGroup != null) { permittedGroups.add(tablePermissions.readWriteGroup); GroupURI permittedGroup = Util.getPermittedGroup(groupClient, permittedGroups); @@ -369,7 +409,7 @@ static void checkTableWritePermissions(TapSchemaDAO dao, String tableName, WebSe // if authenticated check schema owner and readWriteGroup void checkSchemaWritePermissions(TapSchemaDAO dao, String schemaName) - throws AccessControlException, ResourceNotFoundException { + throws AccessControlException, IOException, ResourceNotFoundException { TapPermissions schemaPermissions = dao.getSchemaPermissions(schemaName); if (schemaPermissions == null) { @@ -379,10 +419,8 @@ void checkSchemaWritePermissions(TapSchemaDAO dao, String schemaName) super.logInfo.setMessage("schema write allowed: schema owner"); return; } - LocalAuthority localAuthority = new LocalAuthority(); - URI serviceURI = localAuthority.getServiceURI(Standards.GMS_SEARCH_01.toString()); - GroupClient groupClient = GroupUtil.getGroupClient(serviceURI); - List permittedGroups = new ArrayList(1); + final IvoaGroupClient groupClient = new IvoaGroupClient(); + Set permittedGroups = new TreeSet<>(); if (schemaPermissions.readWriteGroup != null) { permittedGroups.add(schemaPermissions.readWriteGroup); GroupURI permittedGroup = Util.getPermittedGroup(groupClient, permittedGroups); @@ -394,4 +432,35 @@ void checkSchemaWritePermissions(TapSchemaDAO dao, String schemaName) throw new AccessControlException("permission denied"); } + // check if the caller is an admin + boolean checkIsAdmin() { + try { + Context ctx = new InitialContext(); + HttpPrincipal admin = (HttpPrincipal) ctx.lookup(jndiAdminKey); + if (admin != null) { + Subject caller = AuthenticationUtil.getCurrentSubject(); + for (Principal p : caller.getPrincipals()) { + if (AuthenticationUtil.equals(admin, p)) { + return true; + } + } + } + } catch (NamingException ex) { + log.error("Failed to find JNDI key: " + jndiAdminKey, ex); + } + return false; + } + + boolean getCreateSchemaEnabled() { + try { + Context ctx = new InitialContext(); + Boolean ret = (Boolean) ctx.lookup(jndiCreateSchemaKey); + if (ret != null) { + return ret; + } + } catch (NamingException ex) { + log.error("Failed to find JNDI key: " + jndiCreateSchemaKey, ex); + } + return false; + } } diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/Util.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/Util.java index 9b2b31d2..7e2949da 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/Util.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/Util.java @@ -70,16 +70,17 @@ import ca.nrc.cadc.auth.AuthenticationUtil; import ca.nrc.cadc.cred.client.CredUtil; +import ca.nrc.cadc.net.ResourceNotFoundException; import ca.nrc.cadc.tap.schema.TapPermissions; -import java.net.URI; +import java.io.IOException; import java.security.AccessControlException; import java.security.Principal; import java.security.cert.CertificateException; -import java.util.List; +import java.util.Set; import javax.security.auth.Subject; import org.apache.log4j.Logger; -import org.opencadc.gms.GroupClient; import org.opencadc.gms.GroupURI; +import org.opencadc.gms.IvoaGroupClient; /** * Utility class with static methods for checking permissions. @@ -135,10 +136,11 @@ private static boolean isOwner(TapPermissions tp, Subject subject) { return false; } - public static GroupURI getPermittedGroup(GroupClient groupClient, List permittedGroups) { + public static GroupURI getPermittedGroup(IvoaGroupClient groupClient, Set permittedGroups) + throws IOException, ResourceNotFoundException { // no read groups assigned - if (permittedGroups == null || permittedGroups.size() == 0) { + if (permittedGroups == null || permittedGroups.isEmpty()) { return null; } @@ -151,27 +153,17 @@ public static GroupURI getPermittedGroup(GroupClient groupClient, List if (!ensureCredentials()) { throw new AccessControlException("No delegated credentials"); } - // single group membership required - if (permittedGroups.size() == 1) { - if (groupClient.isMember(permittedGroups.get(0))) { - return permittedGroups.get(0); + + try { + // membership in at least one of the groups + Set memberships = groupClient.getMemberships(permittedGroups); + if (memberships == null || memberships.isEmpty()) { + return null; } - return null; + return memberships.iterator().next(); + } catch (InterruptedException ex) { + throw new RuntimeException("UNEXPECTED: " + ex, ex); } - - // membership in at least one of the groups - List memberships = groupClient.getMemberships(); - if (memberships == null || memberships.size() == 0) { - return null; - } - - // remove groups that are not in the list of permitted read groups - memberships.retainAll(permittedGroups); - if (memberships.size() > 0) { - return memberships.get(0); - } - - return null; } // public static GroupURI getWritePermissionsGroup(GroupClient groupClient, TapPermissions permissions) { From 0f142c72efd10c6e61d8557580249b6643049cb5 Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Wed, 17 Jul 2024 10:10:59 -0700 Subject: [PATCH 02/11] youcat: configure admin account configure int-test certificates for admin, owner, member --- youcat/README.md | 19 +++- .../opencadc/youcat/AbstractTablesTest.java | 19 +++- .../org/opencadc/youcat/CreateTableTest.java | 35 ++----- .../opencadc/youcat/LoadTableDataTest.java | 32 +++---- .../org/opencadc/youcat/PermissionsTest.java | 92 ++++++++----------- .../org/opencadc/youcat/YoucatInitAction.java | 65 +++++++++++++ 6 files changed, 160 insertions(+), 102 deletions(-) diff --git a/youcat/README.md b/youcat/README.md index ac685033..bb3babd3 100644 --- a/youcat/README.md +++ b/youcat/README.md @@ -50,9 +50,9 @@ org.opencadc.youcat.uws.url=jdbc:postgresql://{server}/{database} The `tapadm` pool manages (create, alter, drop) tap_schema tables and manages the tap_schema content. The `uws` pool manages (create, alter, drop) uws tables and manages the uws content (creates and modifies jobs in the uws -schema when jobs are created and executed by users. +schema when jobs are created and executed by users. If `youcat` is configured with to create schemas (server _createSchemaInDB_ below) then this pool must also have permission to create schemas. -The `tapuser` pool is used to run TAP queries, including creating tables in the tap_upload schema. +The `tapuser` pool is used to run TAP queries, including creating tables in the `tap_upload` schema. All three pools must have the same JDBC URL (e.g. use the same database) with PostgreSQL. This may be relaxed in future. @@ -80,6 +80,21 @@ See cadc-tap- ## youcat.properties +The youcat.properties configures some admin and optional functions of the service. +``` +# configure the admin user +org.opencadc.youcat.adminUser = {identity} + +# (optional) configure schema creation in the database (default: false) +org.opencadc.youcat.createSchemaInDB = true|false +``` +The admin user can use the youcat API to create a new schema for a user. This will add the +schema to the `tap_schema.schemas` table and enable the user to create tables in that +schema. If the optional _createSchemaInDB_ flag is set to true, a schema created by admin +will be created in the database in addition to being added to the `tap_schema`. If false, +`youcat` will not create the schema in the database and just assume it exists and that the +`tapadm` pool has permission to create objects (tables and indices) in it. + As hard-coded behaviours of `youcat` are extracted from the build and made configurable, the configuration options will usually be in this file (see **development plans** below). diff --git a/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java b/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java index f748a0ab..f0a8b3b4 100644 --- a/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java +++ b/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java @@ -119,16 +119,22 @@ abstract class AbstractTablesTest { private static final Logger log = Logger.getLogger(AbstractTablesTest.class); static { - Log4jInit.setLevel("ca.nrc.cadc.cat", Level.INFO); + Log4jInit.setLevel("org.opencadc.youcat", Level.INFO); Log4jInit.setLevel("ca.nrc.cadc.tap", Level.INFO); } + static final String YOUCAT_ADMIN = "youcat-admin.pem"; // to create test schema + static final String SCHEMA_OWNER_CERT = "youcat-owner.pem"; // own test schema + static final String SCHEMA_GROUP_MEMBER = "youcat-member.pem"; // member of group + static String VALID_TEST_GROUP = "ivo://cadc.nrc.ca/gms?YouCat-ReadWrite"; Subject anon; Subject schemaOwner; Subject subjectWithGroups; + protected String testSchemaName = "int_test_schema"; + URL anonQueryURL; URL certQueryURL; URL anonTablesURL; @@ -139,12 +145,12 @@ abstract class AbstractTablesTest { AbstractTablesTest() { try { - File cf = FileUtil.getFileFromResource("x509_CADCAuthtest1.pem", AbstractTablesTest.class); + File cf = FileUtil.getFileFromResource(SCHEMA_OWNER_CERT, AbstractTablesTest.class); schemaOwner = SSLUtil.createSubject(cf); anon = AuthenticationUtil.getAnonSubject(); log.debug("created schemaOwner: " + schemaOwner); - cf = FileUtil.getFileFromResource("x509_CADCAuthtest2.pem", AbstractTablesTest.class); + cf = FileUtil.getFileFromResource(SCHEMA_GROUP_MEMBER, AbstractTablesTest.class); subjectWithGroups = SSLUtil.createSubject(cf); log.debug("created subjectWithGroups: " + subjectWithGroups); @@ -161,6 +167,9 @@ abstract class AbstractTablesTest { } catch (Exception ex) { log.error("TEST SETUP BUG: failed to find TAP URL", ex); } + + // TODO: use youcat-admin to create the test schema owned by youcat-owner + } catch (Throwable t) { throw new RuntimeException("TEST SETUP FAILED", t); } @@ -192,7 +201,7 @@ TableDesc doCreateTable(Subject subject, String tableName) throws Exception { // cleanup just in case doDelete(subject, tableName, true); - final TableDesc orig = new TableDesc("cadcauthtest1", tableName); + final TableDesc orig = new TableDesc(testSchemaName, tableName); orig.description = "created by intTest"; orig.tableType = TableDesc.TableType.TABLE; orig.tableIndex = 1; @@ -292,7 +301,7 @@ void doCreateIndex(Subject subject, String tableName, String indexCol, boolean u protected void clearSchemaPerms() throws MalformedURLException { TapPermissions tp = new TapPermissions(); tp.isPublic = false; - setPerms(schemaOwner, "cadcauthtest1", tp, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); } protected void setPerms(Subject subject, String name, TapPermissions tp, int expectedCode) throws MalformedURLException { diff --git a/youcat/src/intTest/java/org/opencadc/youcat/CreateTableTest.java b/youcat/src/intTest/java/org/opencadc/youcat/CreateTableTest.java index 62c03793..a8e33565 100644 --- a/youcat/src/intTest/java/org/opencadc/youcat/CreateTableTest.java +++ b/youcat/src/intTest/java/org/opencadc/youcat/CreateTableTest.java @@ -67,11 +67,7 @@ package org.opencadc.youcat; - -import ca.nrc.cadc.auth.AuthMethod; -import ca.nrc.cadc.auth.AuthenticationUtil; import ca.nrc.cadc.auth.RunnableAction; -import ca.nrc.cadc.auth.SSLUtil; import ca.nrc.cadc.dali.tables.TableData; import ca.nrc.cadc.dali.tables.votable.VOTableDocument; import ca.nrc.cadc.dali.tables.votable.VOTableField; @@ -79,34 +75,22 @@ import ca.nrc.cadc.dali.tables.votable.VOTableResource; import ca.nrc.cadc.dali.tables.votable.VOTableTable; import ca.nrc.cadc.dali.tables.votable.VOTableWriter; -import ca.nrc.cadc.net.HttpDelete; import ca.nrc.cadc.net.HttpDownload; -import ca.nrc.cadc.net.HttpPost; import ca.nrc.cadc.net.HttpUpload; import ca.nrc.cadc.net.InputStreamWrapper; import ca.nrc.cadc.net.OutputStreamWrapper; -import ca.nrc.cadc.reg.Standards; -import ca.nrc.cadc.reg.client.RegistryClient; import ca.nrc.cadc.tap.schema.ColumnDesc; import ca.nrc.cadc.tap.schema.TableDesc; import ca.nrc.cadc.tap.schema.TapDataType; import ca.nrc.cadc.tap.schema.TapPermissions; -import ca.nrc.cadc.util.FileUtil; import ca.nrc.cadc.util.Log4jInit; import ca.nrc.cadc.uws.ExecutionPhase; -import ca.nrc.cadc.uws.Job; -import ca.nrc.cadc.uws.JobReader; import ca.nrc.cadc.vosi.InvalidTableSetException; import ca.nrc.cadc.vosi.TableReader; -import ca.nrc.cadc.vosi.TableWriter; import ca.nrc.cadc.vosi.actions.TableDescHandler; -import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.StringReader; import java.net.URL; import java.util.Iterator; import java.util.List; @@ -117,7 +101,6 @@ import org.apache.log4j.Logger; import org.junit.Assert; import org.junit.Test; -import org.opencadc.tap.TapClient; /** * @@ -127,7 +110,7 @@ public class CreateTableTest extends AbstractTablesTest { private static final Logger log = Logger.getLogger(CreateTableTest.class); static { - Log4jInit.setLevel("ca.nrc.cadc.cat", Level.INFO); + Log4jInit.setLevel("org.opencadc.youcat", Level.INFO); Log4jInit.setLevel("ca.nrc.cadc.tap", Level.INFO); } @@ -187,9 +170,9 @@ public void testCreateQueryDropVOSI() { try { clearSchemaPerms(); TapPermissions tp = new TapPermissions(null, true, null, null); - super.setPerms(schemaOwner, "cadcauthtest1", tp, 200); + super.setPerms(schemaOwner, testSchemaName, tp, 200); - String testTable = "cadcauthtest1.testCreateQueryDropVOSI"; + String testTable = testSchemaName + ".testCreateQueryDropVOSI"; final TableDesc orig = doCreateTable(schemaOwner, testTable); TableDesc td = doVosiCheck(testTable); compare(orig, td); @@ -214,9 +197,9 @@ public void testCreateQueryDropVOTable() { try { clearSchemaPerms(); TapPermissions tp = new TapPermissions(null, true, null, null); - super.setPerms(schemaOwner, "cadcauthtest1", tp, 200); + super.setPerms(schemaOwner, testSchemaName, tp, 200); - String testTable = "cadcauthtest1.testCreateQueryDropVOTable"; + String testTable = testSchemaName + ".testCreateQueryDropVOTable"; // cleanup just in case doDelete(schemaOwner, testTable, true); @@ -286,9 +269,9 @@ public void testCreateIndex() { try { clearSchemaPerms(); TapPermissions tp = new TapPermissions(null, true, null, null); - super.setPerms(schemaOwner, "cadcauthtest1", tp, 200); + super.setPerms(schemaOwner, testSchemaName, tp, 200); - String tableName = "cadcauthtest1.testCreateIndex"; + String tableName = testSchemaName + ".testCreateIndex"; TableDesc td = doCreateTable(schemaOwner, tableName); for (ColumnDesc cd : td.getColumnDescs()) { log.info("testCreateIndex: " + cd.getColumnName()); @@ -312,9 +295,9 @@ public void testCreateUniqueIndex() { try { clearSchemaPerms(); TapPermissions tp = new TapPermissions(null, true, null, null); - super.setPerms(schemaOwner, "cadcauthtest1", tp, 200); + super.setPerms(schemaOwner, testSchemaName, tp, 200); - String tableName = "cadcauthtest1.testCreateUniqueIndex"; + String tableName = testSchemaName + ".testCreateUniqueIndex"; TableDesc td = doCreateTable(schemaOwner, tableName); for (ColumnDesc cd : td.getColumnDescs()) { diff --git a/youcat/src/intTest/java/org/opencadc/youcat/LoadTableDataTest.java b/youcat/src/intTest/java/org/opencadc/youcat/LoadTableDataTest.java index 3f34571d..89514c3a 100644 --- a/youcat/src/intTest/java/org/opencadc/youcat/LoadTableDataTest.java +++ b/youcat/src/intTest/java/org/opencadc/youcat/LoadTableDataTest.java @@ -146,7 +146,7 @@ public void testPostNoTableName() { try { log.info("start"); - String testTable = "cadcauthtest1.testPostNoTableName"; + String testTable = testSchemaName + ".testPostNoTableName"; doCreateTable(schemaOwner, testTable); StringBuilder data = new StringBuilder(); @@ -177,7 +177,7 @@ public void testPostInvalidColumnName() { try { log.info("start"); - String testTable = "cadcauthtest1.testPostInvalidColumnName"; + String testTable = testSchemaName + ".testPostInvalidColumnName"; doCreateTable(schemaOwner, testTable); StringBuilder data = new StringBuilder(); @@ -208,7 +208,7 @@ public void testWrongNumberOfColumns() { try { log.info("start"); - String testTable = "cadcauthtest1.testWrongNumberOfColumns"; + String testTable = testSchemaName + ".testWrongNumberOfColumns"; doCreateTable(schemaOwner, testTable); StringBuilder data = new StringBuilder(); @@ -243,7 +243,7 @@ public void testNoSuchTable() { data.append("c0\tc1\n"); data.append("string"); - URL postURL = new URL(certLoadURL.toString() + "/cadcauthtest1.noSuchTable"); + URL postURL = new URL(certLoadURL.toString() + "/" + testSchemaName + ".noSuchTable"); final HttpPost post = new HttpPost(postURL, new FileContent(data.toString(), TableContentHandler.CONTENT_TYPE_TSV, UTF8), false); Subject.doAs(schemaOwner, new PrivilegedExceptionAction() { public Object run() throws Exception { @@ -270,7 +270,7 @@ public void testInvalidTableName() { data.append("c0\tc1\n"); data.append("string"); - URL postURL = new URL(certLoadURL.toString() + "/cadcauthtest1.invalid.table.name"); + URL postURL = new URL(certLoadURL.toString() + "/" + testSchemaName + ".invalid.table.name"); final HttpPost post = new HttpPost(postURL, new FileContent(data.toString(), TableContentHandler.CONTENT_TYPE_TSV, UTF8), false); Subject.doAs(schemaOwner, new PrivilegedExceptionAction() { public Object run() throws Exception { @@ -294,7 +294,7 @@ public void testNotTableOwner() { clearSchemaPerms(); - String testTable = "cadcauthtest1.testNotTableOwner"; + String testTable = testSchemaName + ".testNotTableOwner"; doCreateTable(schemaOwner, testTable); StringBuilder data = new StringBuilder(); @@ -326,9 +326,9 @@ public void testAllDataTypesTSV() { log.info("start"); TapPermissions tp = new TapPermissions(null, true, null, null); - setPerms(schemaOwner, "cadcauthtest1", tp, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); - String testTable = "cadcauthtest1.testAllDataTypesTSV"; + String testTable = testSchemaName + ".testAllDataTypesTSV"; doCreateTable(schemaOwner, testTable); setPerms(schemaOwner, testTable, tp, 200); @@ -397,9 +397,9 @@ public void testAllDataTypesFITS() { log.info("start"); TapPermissions tp = new TapPermissions(null, true, null, null); - setPerms(schemaOwner, "cadcauthtest1", tp, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); - String testTable = "cadcauthtest1.testAllDataTypesFits"; + String testTable = testSchemaName + ".testAllDataTypesFits"; doCreateTable(schemaOwner, testTable); setPerms(schemaOwner, testTable, tp, 200); @@ -506,9 +506,9 @@ public void testMixedContentTypeASCII() { log.info("start"); TapPermissions tp = new TapPermissions(null, true, null, null); - setPerms(schemaOwner, "cadcauthtest1", tp, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); - String testTable = "cadcauthtest1.testMixedContentTypeASCII"; + String testTable = testSchemaName + ".testMixedContentTypeASCII"; doCreateTable(schemaOwner, testTable); setPerms(schemaOwner, testTable, tp, 200); @@ -583,9 +583,9 @@ public void testMultipleBatches() { log.info("start"); TapPermissions tp = new TapPermissions(null, true, null, null); - setPerms(schemaOwner, "cadcauthtest1", tp, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); - String testTable = "cadcauthtest1.testMultipleBatches"; + String testTable = testSchemaName + ".testMultipleBatches"; doCreateTable(schemaOwner, testTable); setPerms(schemaOwner, testTable, tp, 200); @@ -632,9 +632,9 @@ public void testErrorInMiddle() { log.info("start"); TapPermissions tp = new TapPermissions(null, true, null, null); - setPerms(schemaOwner, "cadcauthtest1", tp, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); - String testTable = "cadcauthtest1.testErrorInMiddle"; + String testTable = testSchemaName + ".testErrorInMiddle"; doCreateTable(schemaOwner, testTable); setPerms(schemaOwner, testTable, tp, 200); diff --git a/youcat/src/intTest/java/org/opencadc/youcat/PermissionsTest.java b/youcat/src/intTest/java/org/opencadc/youcat/PermissionsTest.java index 922721f9..b696b7b6 100644 --- a/youcat/src/intTest/java/org/opencadc/youcat/PermissionsTest.java +++ b/youcat/src/intTest/java/org/opencadc/youcat/PermissionsTest.java @@ -67,6 +67,18 @@ package org.opencadc.youcat; +import ca.nrc.cadc.auth.RunnableAction; +import ca.nrc.cadc.dali.tables.TableData; +import ca.nrc.cadc.dali.tables.votable.VOTableDocument; +import ca.nrc.cadc.dali.tables.votable.VOTableReader; +import ca.nrc.cadc.dali.tables.votable.VOTableResource; +import ca.nrc.cadc.dali.tables.votable.VOTableTable; +import ca.nrc.cadc.net.FileContent; +import ca.nrc.cadc.net.HttpDownload; +import ca.nrc.cadc.net.HttpPost; +import ca.nrc.cadc.tap.schema.TapPermissions; +import ca.nrc.cadc.uws.ExecutionPhase; +import ca.nrc.cadc.vosi.actions.TableContentHandler; import java.io.ByteArrayOutputStream; import java.net.MalformedURLException; import java.net.URI; @@ -77,29 +89,13 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; - import javax.security.auth.Subject; import javax.security.auth.x500.X500Principal; - import org.apache.log4j.Logger; import org.junit.Assert; import org.junit.Test; import org.opencadc.gms.GroupURI; -import ca.nrc.cadc.auth.HttpPrincipal; -import ca.nrc.cadc.auth.RunnableAction; -import ca.nrc.cadc.dali.tables.TableData; -import ca.nrc.cadc.dali.tables.votable.VOTableDocument; -import ca.nrc.cadc.dali.tables.votable.VOTableReader; -import ca.nrc.cadc.dali.tables.votable.VOTableResource; -import ca.nrc.cadc.dali.tables.votable.VOTableTable; -import ca.nrc.cadc.net.FileContent; -import ca.nrc.cadc.net.HttpDownload; -import ca.nrc.cadc.net.HttpPost; -import ca.nrc.cadc.tap.schema.TapPermissions; -import ca.nrc.cadc.uws.ExecutionPhase; -import ca.nrc.cadc.vosi.actions.TableContentHandler; - /** * * @author majorb @@ -118,12 +114,11 @@ public void testAnon() { try { clearSchemaPerms(); - String testSchema = "cadcauthtest1"; - String testTable = testSchema + ".testGetAnon"; + String testTable = testSchemaName + ".testGetAnon"; doCreateTable(schemaOwner, testTable); ByteArrayOutputStream out = new ByteArrayOutputStream(); - URL schemaPerms = new URL(permsURL.toString() + "/" + testSchema); + URL schemaPerms = new URL(permsURL.toString() + "/" + testSchemaName); URL tablePerms = new URL(permsURL.toString() + "/" + testTable); // get schema perms @@ -169,8 +164,7 @@ public void testBadSetParams() { try { clearSchemaPerms(); - String testSchema = "cadcauthtest1"; - String testTable = testSchema + ".testBadSetParams"; + String testTable = testSchemaName + ".testBadSetParams"; doCreateTable(schemaOwner, testTable); URL tablePerms = new URL(permsURL.toString() + "/" + testTable); @@ -223,14 +217,13 @@ public void testPublic() { try { clearSchemaPerms(); - String testSchema = "cadcauthtest1"; - String testTable = testSchema + ".testPublic"; + String testTable = testSchemaName + ".testPublic"; doCreateTable(schemaOwner, testTable); this.doQuery(anon, anonQueryURL, testTable, 400); TapPermissions tp = new TapPermissions(null, true, null, null); - setPerms(schemaOwner, testSchema, tp, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); this.doQuery(anon, anonQueryURL, testTable, 403); setPerms(schemaOwner, testTable, tp, 200); @@ -252,8 +245,7 @@ public void testGroupRead() { clearSchemaPerms(); - String testSchema = "cadcauthtest1"; - String testTable = testSchema + ".testGroupRead"; + String testTable = testSchemaName + ".testGroupRead"; doCreateTable(schemaOwner, testTable); this.doQuery(subjectWithGroups, certQueryURL, testTable, 400); @@ -262,8 +254,8 @@ public void testGroupRead() { GroupURI readGroup = new GroupURI(VALID_TEST_GROUP); TapPermissions tp = new TapPermissions(null, false, readGroup, null); - setPerms(schemaOwner, testSchema, tp, 200); - TapPermissions tp1 = getPermissions(schemaOwner, testSchema, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); + TapPermissions tp1 = getPermissions(schemaOwner, testSchemaName, 200); Assert.assertNotNull(tp1.isPublic); Assert.assertFalse(tp1.isPublic); Assert.assertEquals(readGroup, tp1.readGroup); @@ -297,8 +289,7 @@ public void testGroupReadWrite() { try { clearSchemaPerms(); - String testSchema = "cadcauthtest1"; - String testTable = testSchema + ".testGroupReadWrite"; + String testTable = testSchemaName + ".testGroupReadWrite"; doCreateTable(schemaOwner, testTable); this.doQuery(subjectWithGroups, certQueryURL, testTable, 400); @@ -307,8 +298,8 @@ public void testGroupReadWrite() { GroupURI readWriteGroup = new GroupURI(VALID_TEST_GROUP); TapPermissions tp = new TapPermissions(null, false, null, readWriteGroup); - setPerms(schemaOwner, testSchema, tp, 200); - TapPermissions tp1 = getPermissions(schemaOwner, testSchema, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); + TapPermissions tp1 = getPermissions(schemaOwner, testSchemaName, 200); Assert.assertNotNull(tp1.isPublic); Assert.assertFalse(tp1.isPublic); Assert.assertNull(tp1.readGroup); @@ -342,11 +333,10 @@ public void testSchemaOwnerDropTable() { try { clearSchemaPerms(); - String testSchema = "cadcauthtest1"; - String testTable = testSchema + ".testDropTable"; + String testTable = testSchemaName + ".testDropTable"; TapPermissions tp = new TapPermissions(null, true, null, new GroupURI(VALID_TEST_GROUP)); - setPerms(schemaOwner, testSchema, tp, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); doCreateTable(subjectWithGroups, testTable); this.doQuery(subjectWithGroups, certQueryURL, testTable, 200); @@ -368,11 +358,10 @@ public void testDropTable() { try { clearSchemaPerms(); - String testSchema = "cadcauthtest1"; - String testTable = testSchema + ".testDropTable"; + String testTable = testSchemaName + ".testDropTable"; TapPermissions tp = new TapPermissions(null, true, null, new GroupURI(VALID_TEST_GROUP)); - setPerms(schemaOwner, testSchema, tp, 200); + setPerms(schemaOwner, testSchemaName, tp, 200); doCreateTable(subjectWithGroups, testTable); this.doQuery(subjectWithGroups, certQueryURL, testTable, 200); @@ -394,21 +383,19 @@ public void testNoInheritance() { log.info("testNoInheritance()"); try { - String testSchema = "cadcauthtest1"; - GroupURI group1 = new GroupURI("ivo://cadc.nrc.ca/gms?group1"); GroupURI group2 = new GroupURI("ivo://cadc.nrc.ca/gms?group2"); TapPermissions tp = new TapPermissions(null, true, group1, group2); - this.setPerms(schemaOwner, testSchema, tp, 200); + this.setPerms(schemaOwner, testSchemaName, tp, 200); - TapPermissions actual = this.getPermissions(schemaOwner, testSchema, 200); + TapPermissions actual = this.getPermissions(schemaOwner, testSchemaName, 200); Assert.assertTrue(actual.owner.getPrincipals(X500Principal.class).iterator().next() .getName().equals("CN=cadcauthtest1_24c,OU=cadc,O=hia,C=ca")); Assert.assertEquals(true, actual.isPublic); Assert.assertEquals(group1, actual.readGroup); Assert.assertEquals(group2, actual.readWriteGroup); - String testTable = testSchema + ".testNoInheritance"; + String testTable = testSchemaName + ".testNoInheritance"; doCreateTable(schemaOwner, testTable); actual = this.getPermissions(schemaOwner, testTable, 200); @@ -435,8 +422,7 @@ public void testQueriesChangingPerms() { // query tap_schema.schemas -- null owner so should be public this.doQuery(anon, anonQueryURL, "tap_schema.schemas", 200); - String testSchema = "cadcauthtest1"; - String testTable = testSchema + ".testQueriesChangingPerms"; + String testTable = testSchemaName + ".testQueriesChangingPerms"; doCreateTable(schemaOwner, testTable); // initially private @@ -446,7 +432,7 @@ public void testQueriesChangingPerms() { // set schema and table to public TapPermissions tp = new TapPermissions(null, true, null, null); - this.setPerms(schemaOwner, testSchema, tp, 200); + this.setPerms(schemaOwner, testSchemaName, tp, 200); this.setPerms(schemaOwner, testTable, tp, 200); this.doQuery(anon, certQueryURL, testTable, 200); this.doQuery(subjectWithGroups, certQueryURL, testTable, 200); @@ -524,7 +510,7 @@ public void testGroupAccessQuerySchemasTable() { GroupURI readGroup = new GroupURI(VALID_TEST_GROUP); TapPermissions tp = new TapPermissions(null, false, readGroup, null); - this.setPerms(this.schemaOwner, "cadcauthtest1", tp, 200); + this.setPerms(this.schemaOwner, testSchemaName, tp, 200); String query = "select schema_name from tap_schema.schemas"; @@ -581,7 +567,7 @@ public void testGroupAccessQueryTablesTable() { GroupURI readGroup = new GroupURI(VALID_TEST_GROUP); TapPermissions tp = new TapPermissions(null, false, readGroup, null); - this.setPerms(this.schemaOwner, "cadcauthtest1", tp, 200); + this.setPerms(this.schemaOwner, testSchemaName, tp, 200); String query = "select schema_name from tap_schema.tables"; @@ -641,7 +627,7 @@ public void testGroupQueryColumnsTable() { GroupURI readGroup = new GroupURI(VALID_TEST_GROUP); TapPermissions tp = new TapPermissions(null, false, readGroup, null); - this.setPerms(this.schemaOwner, "cadcauthtest1", tp, 200); + this.setPerms(this.schemaOwner, testSchemaName, tp, 200); String query = "select t.schema_name from tap_schema.tables t " + "join tap_schema.columns c on t.table_name=c.table_name"; @@ -678,7 +664,7 @@ private void assertAnonymousSchemaResults(VOTableDocument doc) { if (((String) row.get(0)).equals("tap_schema")) { foundTapSchemaSchema = true; } - if (((String) row.get(0)).equals("cadcauthtest1")) { + if (((String) row.get(0)).equals(testSchemaName)) { foundCadcauthtest1Schema = true; } } @@ -686,7 +672,7 @@ private void assertAnonymousSchemaResults(VOTableDocument doc) { Assert.fail("failed to find tap schema schema"); } if (foundCadcauthtest1Schema) { - Assert.fail("mistakenly found cadcauthtest1 schema"); + Assert.fail("mistakenly found " + testSchemaName + " schema"); } } catch (Throwable t) { @@ -716,7 +702,7 @@ private void assertAuthtest1ReadResults(VOTableDocument doc) { if (((String) row.get(0)).equals("tap_schema")) { foundTapSchemaSchema = true; } - if (((String) row.get(0)).equals("cadcauthtest1")) { + if (((String) row.get(0)).equals(testSchemaName)) { foundCadcauthtest1Schema = true; } } @@ -724,7 +710,7 @@ private void assertAuthtest1ReadResults(VOTableDocument doc) { Assert.fail("failed to find tap schema schema"); } if (!foundCadcauthtest1Schema) { - Assert.fail("failed to find cadcauthtest1 schema"); + Assert.fail("failed to find " + testSchemaName + " schema"); } } catch (Throwable t) { log.error("unexpected", t); diff --git a/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java b/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java index 8d777e1d..a4a31712 100644 --- a/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java +++ b/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java @@ -67,10 +67,17 @@ package org.opencadc.youcat; +import ca.nrc.cadc.auth.HttpPrincipal; import ca.nrc.cadc.db.DBUtil; import ca.nrc.cadc.rest.InitAction; import ca.nrc.cadc.tap.schema.InitDatabaseTS; +import ca.nrc.cadc.util.InvalidConfigException; +import ca.nrc.cadc.util.MultiValuedProperties; +import ca.nrc.cadc.util.PropertiesReader; import ca.nrc.cadc.uws.server.impl.InitDatabaseUWS; +import ca.nrc.cadc.vosi.actions.TablesAction; +import javax.naming.Context; +import javax.naming.InitialContext; import javax.sql.DataSource; import org.apache.log4j.Logger; @@ -81,12 +88,68 @@ public class YoucatInitAction extends InitAction { private static final Logger log = Logger.getLogger(YoucatInitAction.class); + private static final String YOUCAT = YoucatInitAction.class.getPackageName(); + private static final String YOUCAT_ADMIN = YOUCAT + ".adminUser"; + private static final String YOUCAT_CREATE = YOUCAT + ".createSchemaInDB"; + + private String jndiAdminKey; + private String jndiCreateSchemaKey; + public YoucatInitAction() { } + private void initConfig() { + this.jndiAdminKey = appName + TablesAction.ADMIN_KEY; + this.jndiCreateSchemaKey = appName + TablesAction.CREATE_SCHEMA_KEY; + + PropertiesReader r = new PropertiesReader("youcat.properties"); + MultiValuedProperties mvp = r.getAllProperties(); + + StringBuilder sb = new StringBuilder(); + sb.append("incomplete config: "); + boolean ok = true; + + String username = mvp.getFirstPropertyValue(YOUCAT_ADMIN); + sb.append("\n\t" + YOUCAT_ADMIN + ": "); + if (username == null) { + sb.append("MISSING"); + ok = false; + } else { + sb.append("OK"); + } + + String yc = mvp.getFirstPropertyValue(YOUCAT_CREATE); + sb.append("\n\t" + YOUCAT_CREATE + ": "); + if (yc == null) { + sb.append("MISSING"); + } else { + sb.append("OK"); + } + + if (!ok) { + throw new InvalidConfigException(sb.toString()); + } + + HttpPrincipal hp = new HttpPrincipal(username); + Boolean createSchemaInDB = true; + if (yc != null && "false".equals(yc)) { + createSchemaInDB = false; + } + try { + Context ctx = new InitialContext(); + ctx.bind(jndiAdminKey, hp); + ctx.bind(jndiCreateSchemaKey, createSchemaInDB); + log.info("init: admin=" + hp + " createSchemaInDB=" + createSchemaInDB); + } catch (Exception ex) { + log.error("Failed to create JNDI key(s): " + jndiAdminKey + "|" + jndiCreateSchemaKey, ex); + } + } + @Override public void doInit() { try { + initConfig(); + // tap_schema log.info("InitDatabaseTS: START"); DataSource tapadm = DBUtil.findJNDIDataSource("jdbc/tapadm"); @@ -100,6 +163,8 @@ public void doInit() { InitDatabaseUWS uwsi = new InitDatabaseUWS(uws, null, "uws"); uwsi.doInit(); log.info("InitDatabaseUWS: OK"); + + } catch (Exception ex) { throw new RuntimeException("INIT FAIL: " + ex.getMessage(), ex); } From 0ed45508a13c833fef1e392c3f0b0023aa2d2b66 Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Wed, 17 Jul 2024 10:20:04 -0700 Subject: [PATCH 03/11] youcat: add intTest README --- youcat/src/intTest/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 youcat/src/intTest/README.md diff --git a/youcat/src/intTest/README.md b/youcat/src/intTest/README.md new file mode 100644 index 00000000..d2825a4f --- /dev/null +++ b/youcat/src/intTest/README.md @@ -0,0 +1,19 @@ +# youcat local integration tests + +The integration tests lookup and test `ivo://opencadc.org/youcat` which should only +ever be found in a local DEVELOPMENT registry. It is not safe to run the tests against +a database where you care about the content! + +The tests use a schema named `int_test_schema`. + +The following x509 client certificates are found in `$A/test-certificates` and used to +execute the tests: +- youcat-admin.pem : used to create the test schema, same ident must be configured as the admin in the deployed service +- youcat-owner.pem : the owner of the test schema +- youcat-member.pem : member of a group that you-cat-owner grants read-write access to + +The tests are curently only runnable by CADC staff because of ard coded setup and assumptions about external +content (group in GMS). The test group is currently hard coded to one in the `ivo://cadc.nrc.ca/gms` GMS service. + +Tested: youcat-admin == personal account, youcat-owner == cadcauthtest1, youcat-member == cadcauthtest2 + From c36ed8564f1c92749bf3f45d3c7385f371e0abe1 Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Wed, 17 Jul 2024 10:23:03 -0700 Subject: [PATCH 04/11] fix imports --- .../intTest/java/org/opencadc/youcat/AbstractTablesTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java b/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java index f0a8b3b4..cc7e9d32 100644 --- a/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java +++ b/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java @@ -67,7 +67,6 @@ package org.opencadc.youcat; - import ca.nrc.cadc.auth.AuthMethod; import ca.nrc.cadc.auth.AuthenticationUtil; import ca.nrc.cadc.auth.RunnableAction; @@ -108,7 +107,6 @@ import org.apache.log4j.Logger; import org.junit.Assert; import org.opencadc.tap.TapClient; -import org.postgresql.shaded.com.ongres.scram.common.util.StringWritable; /** * base class with common setup and methods. From b4131dd595577b84de05cb7cc8b36ea2dd55acbb Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Thu, 18 Jul 2024 09:52:06 -0700 Subject: [PATCH 05/11] cadc-tap-schema: improve batch inserts --- .../java/ca/nrc/cadc/tap/db/TableLoader.java | 138 ++++++++++-------- 1 file changed, 75 insertions(+), 63 deletions(-) diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/tap/db/TableLoader.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/tap/db/TableLoader.java index 5de8145c..f5dba766 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/tap/db/TableLoader.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/tap/db/TableLoader.java @@ -67,6 +67,7 @@ package ca.nrc.cadc.tap.db; +import static com.sun.tools.javac.jvm.ByteCodes.ret; import ca.nrc.cadc.dali.Circle; import ca.nrc.cadc.dali.DoubleInterval; import ca.nrc.cadc.dali.Interval; @@ -81,10 +82,10 @@ import ca.nrc.cadc.tap.schema.TableDesc; import ca.nrc.cadc.tap.schema.TapDataType; import java.net.URI; -import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.Iterator; @@ -94,7 +95,7 @@ import org.opencadc.tap.io.InconsistentTableDataException; import org.opencadc.tap.io.TableDataInputStream; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter; /** * Utility to bulk load content into a table. @@ -142,25 +143,29 @@ public void load(TableDesc destTable, TableDataInputStream data) { Iterator> dataIterator = data.iterator(); List nextRow = null; + List> batch = new ArrayList<>(batchSize); int count = 0; try { while (!done) { count = 0; tm.startTransaction(); prof.checkpoint("start-transaction"); + BulkInsertStatement bulkInsertStatement = new BulkInsertStatement(reorgTable); - while (count < batchSize && dataIterator.hasNext()) { + while (batch.size() < batchSize && dataIterator.hasNext()) { nextRow = dataIterator.next(); convertValueObjects(nextRow); - jdbc.update(sql, nextRow.toArray()); + batch.add(nextRow); count++; } - log.debug("Inserting " + count + " rows in this batch."); + log.debug("Inserting " + batch.size() + " rows in this batch."); + jdbc.batchUpdate(sql, batch, batchSize, bulkInsertStatement); prof.checkpoint("batch-of-inserts"); tm.commitTransaction(); prof.checkpoint("commit-transaction"); - totalInserts += count; + totalInserts += batch.size(); + batch.clear(); done = !dataIterator.hasNext(); } } catch (IllegalArgumentException | IndexOutOfBoundsException | InconsistentTableDataException ex) { @@ -178,8 +183,8 @@ public void load(TableDesc destTable, TableDataInputStream data) { } catch (Exception oops) { log.error("Unexpected: could not rollback transaction", oops); } - throw new IllegalArgumentException("Inserted " + totalInserts + " rows. " + - "Current batch failed with: " + ex.getMessage() + " on line " + (totalInserts + count)); + throw new IllegalArgumentException("Inserted " + totalInserts + " rows." + + " Current batch failed with: " + ex.getMessage() + " on line " + (totalInserts + count)); } catch (Throwable t) { try { data.close(); @@ -197,8 +202,8 @@ public void load(TableDesc destTable, TableDataInputStream data) { } log.debug("Batch insert failure", t); - throw new RuntimeException("Inserted " + totalInserts + " rows. " - + "Current batch of " + batchSize + " failed with: " + t.getMessage(), t); + throw new RuntimeException("Inserted " + totalInserts + " rows." + + " Current batch of " + batchSize + " failed with: " + t.getMessage(), t); } finally { if (tm.isOpen()) { @@ -241,27 +246,26 @@ private String generateInsertSQL(TableDesc td) { return sb.toString(); } - private class BulkInsertStatement implements PreparedStatementCreator { + private class BulkInsertStatement implements ParameterizedPreparedStatementSetter> { private final Calendar utc = Calendar.getInstance(DateUtil.UTC); private TableDesc tableDesc; - Object[] row; + public BulkInsertStatement(TableDesc tableDesc) { + this.tableDesc = tableDesc; + } + @Override - public PreparedStatement createPreparedStatement(Connection con) throws SQLException { - String sql = generateInsertSQL(tableDesc); - PreparedStatement ret = con.prepareStatement(sql); - - for (int i = 0; i < tableDesc.getColumnDescs().size(); i++) { - ColumnDesc cd = tableDesc.getColumnDescs().get(i); - Object val = row[i]; + public void setValues(PreparedStatement ps, List row) throws SQLException { + int col = 1; + for (Object val : row) { + ColumnDesc cd = tableDesc.getColumnDescs().get(col - 1); if (val != null && val instanceof Date && TapDataType.TIMESTAMP.equals(cd.getDatatype())) { Date d = (Date) val; - ret.setTimestamp(i + 1, new Timestamp(d.getTime()), utc); + ps.setTimestamp(col++, new Timestamp(d.getTime()), utc); } else { - ret.setObject(i + 1, val); + ps.setObject(col++, val); } } - return ret; } } @@ -274,52 +278,60 @@ public long getTotalInserts() { // convert values that the JDBC driver won't accept private void convertValueObjects(List values) { - for (int i=0; i < values.size(); i++) { + for (int i = 0; i < values.size(); i++) { Object v = values.get(i); if (v != null) { - if (v instanceof URI) { - String nv = ((URI) v).toASCIIString(); - values.set(i, nv); - } else if (v instanceof DoubleInterval) { - Object nv = ddType.getIntervalObject((DoubleInterval) v); - values.set(i, nv); - } else if (v instanceof LongInterval) { - Interval inter = (Interval) v; - DoubleInterval di = new DoubleInterval(inter.getLower().doubleValue(), inter.getUpper().doubleValue()); - Object nv = ddType.getIntervalObject(di); - values.set(i, nv); - } else if (v instanceof Point) { - Object nv = ddType.getPointObject((Point) v); - values.set(i, nv); - } else if (v instanceof Circle) { - Object nv = ddType.getCircleObject((Circle) v); - values.set(i, nv); - } else if (v instanceof Polygon) { - Object nv = ddType.getPolygonObject((Polygon) v); - values.set(i, nv); - } else if (v instanceof ca.nrc.cadc.stc.Position) { - Object nv = ddType.getPointObject((ca.nrc.cadc.stc.Position) v); - values.set(i, nv); - } else if (v instanceof ca.nrc.cadc.stc.Region) { - Object nv = ddType.getRegionObject((ca.nrc.cadc.stc.Region) v); - values.set(i, nv); - } else if (v instanceof short[]) { - Object nv = ddType.getArrayObject((short[]) v); - values.set(i, nv); - } else if (v instanceof int[]) { - Object nv = ddType.getArrayObject((int[]) v); - values.set(i, nv); - } else if (v instanceof long[]) { - Object nv = ddType.getArrayObject((long[]) v); - values.set(i, nv); - } else if (v instanceof float[]) { - Object nv = ddType.getArrayObject((float[]) v); - values.set(i, nv); - } else if (v instanceof double[]) { - Object nv = ddType.getArrayObject((double[]) v); + Object nv = convertValueObject(v); + if (v != nv) { values.set(i, nv); } } } } + + private Object convertValueObject(Object v) { + if (v instanceof URI) { + return ((URI) v).toASCIIString(); + } + if (v instanceof DoubleInterval) { + return ddType.getIntervalObject((DoubleInterval) v); + } + if (v instanceof LongInterval) { + Interval inter = (Interval) v; + DoubleInterval di = new DoubleInterval(inter.getLower().doubleValue(), inter.getUpper().doubleValue()); + return ddType.getIntervalObject(di); + } + if (v instanceof Point) { + return ddType.getPointObject((Point) v); + } + if (v instanceof Circle) { + return ddType.getCircleObject((Circle) v); + } + if (v instanceof Polygon) { + return ddType.getPolygonObject((Polygon) v); + } + if (v instanceof ca.nrc.cadc.stc.Position) { + return ddType.getPointObject((ca.nrc.cadc.stc.Position) v); + } + if (v instanceof ca.nrc.cadc.stc.Region) { + return ddType.getRegionObject((ca.nrc.cadc.stc.Region) v); + } + if (v instanceof short[]) { + return ddType.getArrayObject((short[]) v); + } + if (v instanceof int[]) { + return ddType.getArrayObject((int[]) v); + } + if (v instanceof long[]) { + return ddType.getArrayObject((long[]) v); + } + if (v instanceof float[]) { + return ddType.getArrayObject((float[]) v); + } + if (v instanceof double[]) { + return ddType.getArrayObject((double[]) v); + } + + return v; + } } From 3ccfcbf749e93f7f7f9b573a076f376a4e54555a Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Thu, 18 Jul 2024 11:50:11 -0700 Subject: [PATCH 06/11] fix bad import --- .../src/main/java/ca/nrc/cadc/tap/db/TableLoader.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/tap/db/TableLoader.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/tap/db/TableLoader.java index f5dba766..5eac3317 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/tap/db/TableLoader.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/tap/db/TableLoader.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2018. (c) 2018. +* (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 @@ -67,7 +67,6 @@ package ca.nrc.cadc.tap.db; -import static com.sun.tools.javac.jvm.ByteCodes.ret; import ca.nrc.cadc.dali.Circle; import ca.nrc.cadc.dali.DoubleInterval; import ca.nrc.cadc.dali.Interval; From e45731b36d4406beb603bc25cd2de86c6502d174 Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Thu, 18 Jul 2024 12:02:37 -0700 Subject: [PATCH 07/11] youcat: fix init to make admin fully optional --- youcat/README.md | 14 ++++++------ .../org/opencadc/youcat/YoucatInitAction.java | 22 ++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/youcat/README.md b/youcat/README.md index bb3babd3..903da4c7 100644 --- a/youcat/README.md +++ b/youcat/README.md @@ -82,18 +82,18 @@ See cadc-tap- The youcat.properties configures some admin and optional functions of the service. ``` -# configure the admin user +# (optional) configure the admin user org.opencadc.youcat.adminUser = {identity} -# (optional) configure schema creation in the database (default: false) +# (optional) schema creation in the database (default: false) org.opencadc.youcat.createSchemaInDB = true|false ``` The admin user can use the youcat API to create a new schema for a user. This will add the -schema to the `tap_schema.schemas` table and enable the user to create tables in that -schema. If the optional _createSchemaInDB_ flag is set to true, a schema created by admin -will be created in the database in addition to being added to the `tap_schema`. If false, -`youcat` will not create the schema in the database and just assume it exists and that the -`tapadm` pool has permission to create objects (tables and indices) in it. +schema to the `tap_schema.schemas` table with the specified owner and enable the owner to +further manage that schema. If the optional _createSchemaInDB_ flag is set to true, a schema +created by admin will be created in the database in addition to being added to the `tap_schema`. +If false, `youcat` will not create the schema in the database and just assume it exists and +that the `tapadm` pool has permission to create objects (tables and indices) in it. As hard-coded behaviours of `youcat` are extracted from the build and made configurable, the configuration options will usually be in this file (see **development plans** below). diff --git a/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java b/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java index a4a31712..22a2cbbd 100644 --- a/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java +++ b/youcat/src/main/java/org/opencadc/youcat/YoucatInitAction.java @@ -109,11 +109,10 @@ private void initConfig() { sb.append("incomplete config: "); boolean ok = true; - String username = mvp.getFirstPropertyValue(YOUCAT_ADMIN); + String adminUser = mvp.getFirstPropertyValue(YOUCAT_ADMIN); sb.append("\n\t" + YOUCAT_ADMIN + ": "); - if (username == null) { + if (adminUser == null) { sb.append("MISSING"); - ok = false; } else { sb.append("OK"); } @@ -130,16 +129,19 @@ private void initConfig() { throw new InvalidConfigException(sb.toString()); } - HttpPrincipal hp = new HttpPrincipal(username); - Boolean createSchemaInDB = true; - if (yc != null && "false".equals(yc)) { - createSchemaInDB = false; + Boolean createSchemaInDB = false; // default: false for backwards compat + if (yc != null && "true".equals(yc)) { + createSchemaInDB = true; } try { Context ctx = new InitialContext(); - ctx.bind(jndiAdminKey, hp); - ctx.bind(jndiCreateSchemaKey, createSchemaInDB); - log.info("init: admin=" + hp + " createSchemaInDB=" + createSchemaInDB); + if (adminUser != null) { + ctx.bind(jndiAdminKey, new HttpPrincipal(adminUser)); + } + if (createSchemaInDB != null) { + 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); } From 6ad30dd4603af03353c158c68c996a4e4f811bfb Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Fri, 19 Jul 2024 14:22:41 -0700 Subject: [PATCH 08/11] Update youcat/src/intTest/README.md Co-authored-by: Adrian --- youcat/src/intTest/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youcat/src/intTest/README.md b/youcat/src/intTest/README.md index d2825a4f..01e2270e 100644 --- a/youcat/src/intTest/README.md +++ b/youcat/src/intTest/README.md @@ -10,7 +10,7 @@ The following x509 client certificates are found in `$A/test-certificates` and u execute the tests: - youcat-admin.pem : used to create the test schema, same ident must be configured as the admin in the deployed service - youcat-owner.pem : the owner of the test schema -- youcat-member.pem : member of a group that you-cat-owner grants read-write access to +- youcat-member.pem : member of a group that youcat-owner grants read-write access to The tests are curently only runnable by CADC staff because of ard coded setup and assumptions about external content (group in GMS). The test group is currently hard coded to one in the `ivo://cadc.nrc.ca/gms` GMS service. From dbb806b112c4028e820c72ceb341ff2874cb605c Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Fri, 19 Jul 2024 14:23:33 -0700 Subject: [PATCH 09/11] Update youcat/src/intTest/README.md Co-authored-by: Adrian --- youcat/src/intTest/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youcat/src/intTest/README.md b/youcat/src/intTest/README.md index 01e2270e..e109a0a6 100644 --- a/youcat/src/intTest/README.md +++ b/youcat/src/intTest/README.md @@ -12,7 +12,7 @@ execute the tests: - youcat-owner.pem : the owner of the test schema - youcat-member.pem : member of a group that youcat-owner grants read-write access to -The tests are curently only runnable by CADC staff because of ard coded setup and assumptions about external +The tests are curently only runnable by CADC staff because of hard coded setup and assumptions about external content (group in GMS). The test group is currently hard coded to one in the `ivo://cadc.nrc.ca/gms` GMS service. Tested: youcat-admin == personal account, youcat-owner == cadcauthtest1, youcat-member == cadcauthtest2 From 0b5c0872129295660f728610cf4892d81003eba3 Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Fri, 19 Jul 2024 14:31:08 -0700 Subject: [PATCH 10/11] clarify adminUser in README --- youcat/README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/youcat/README.md b/youcat/README.md index 903da4c7..e40f13df 100644 --- a/youcat/README.md +++ b/youcat/README.md @@ -88,12 +88,15 @@ org.opencadc.youcat.adminUser = {identity} # (optional) schema creation in the database (default: false) org.opencadc.youcat.createSchemaInDB = true|false ``` -The admin user can use the youcat API to create a new schema for a user. This will add the -schema to the `tap_schema.schemas` table with the specified owner and enable the owner to -further manage that schema. If the optional _createSchemaInDB_ flag is set to true, a schema -created by admin will be created in the database in addition to being added to the `tap_schema`. -If false, `youcat` will not create the schema in the database and just assume it exists and -that the `tapadm` pool has permission to create objects (tables and indices) in it. +The optional _adminUser_ (configured using the network username) can use the youcat API to create a +new schema for a user. This will add the schema to the `tap_schema.schemas` table with the +specified owner and enable the owner to further manage that schema. If not configured, creating a +schema through the REST API is not permitted. + +The optional _createSchemaInDB_ flag is set to true, a schema created by admin will be created in +the database in addition to being added to the `tap_schema`. If false, `youcat` will not create +the schema in the database and just assume it exists and that the `tapadm` pool has permission +to create objects (tables and indices) in it. As hard-coded behaviours of `youcat` are extracted from the build and made configurable, the configuration options will usually be in this file (see **development plans** below). From dc8f78753b35d75ba44b651928ce14faf394811a Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Fri, 19 Jul 2024 14:36:28 -0700 Subject: [PATCH 11/11] update copright date and clean imports --- .../java/ca/nrc/cadc/vosi/actions/GetAction.java | 2 +- .../cadc/vosi/actions/GetPermissionsAction.java | 2 +- .../cadc/vosi/actions/PostPermissionsAction.java | 14 ++++++-------- .../java/ca/nrc/cadc/vosi/actions/PutAction.java | 3 +-- .../ca/nrc/cadc/vosi/actions/TableDescHandler.java | 5 +---- .../ca/nrc/cadc/vosi/actions/TablesAction.java | 3 +-- .../main/java/ca/nrc/cadc/vosi/actions/Util.java | 2 +- .../org/opencadc/youcat/AbstractTablesTest.java | 2 +- .../java/org/opencadc/youcat/CreateTableTest.java | 2 +- .../org/opencadc/youcat/LoadTableDataTest.java | 2 +- .../java/org/opencadc/youcat/PermissionsTest.java | 2 +- 11 files changed, 16 insertions(+), 23 deletions(-) diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetAction.java index 62ce245a..b153ed08 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetAction.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2018. (c) 2018. +* (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 diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetPermissionsAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetPermissionsAction.java index ba689ac2..0dc8c49a 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetPermissionsAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/GetPermissionsAction.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2018. (c) 2018. +* (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 diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PostPermissionsAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PostPermissionsAction.java index 6fd0a45a..59cfdb4e 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PostPermissionsAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PostPermissionsAction.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2018. (c) 2018. +* (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 @@ -67,6 +67,11 @@ package ca.nrc.cadc.vosi.actions; +import ca.nrc.cadc.net.ResourceNotFoundException; +import ca.nrc.cadc.rest.InlineContentException; +import ca.nrc.cadc.rest.InlineContentHandler; +import ca.nrc.cadc.tap.schema.TapPermissions; +import ca.nrc.cadc.tap.schema.TapSchemaDAO; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -74,16 +79,9 @@ import java.net.URI; import java.util.HashMap; import java.util.Map; - import org.apache.log4j.Logger; import org.opencadc.gms.GroupURI; -import ca.nrc.cadc.net.ResourceNotFoundException; -import ca.nrc.cadc.rest.InlineContentException; -import ca.nrc.cadc.rest.InlineContentHandler; -import ca.nrc.cadc.tap.schema.TapPermissions; -import ca.nrc.cadc.tap.schema.TapSchemaDAO; - /** * Set the permissions on a schema or table. * diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PutAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PutAction.java index 2c06ee9f..0146d85e 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PutAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/PutAction.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2018. (c) 2018. +* (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 @@ -69,7 +69,6 @@ import ca.nrc.cadc.auth.AuthenticationUtil; import ca.nrc.cadc.auth.HttpPrincipal; -import ca.nrc.cadc.auth.IdentityManager; import ca.nrc.cadc.db.DatabaseTransactionManager; import ca.nrc.cadc.net.ResourceAlreadyExistsException; import ca.nrc.cadc.profiler.Profiler; diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TableDescHandler.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TableDescHandler.java index 5800ed59..dda974a1 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TableDescHandler.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TableDescHandler.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2018. (c) 2018. +* (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 @@ -67,7 +67,6 @@ package ca.nrc.cadc.vosi.actions; - import ca.nrc.cadc.dali.tables.votable.VOTableDocument; import ca.nrc.cadc.dali.tables.votable.VOTableReader; import ca.nrc.cadc.dali.tables.votable.VOTableResource; @@ -77,12 +76,10 @@ import ca.nrc.cadc.rest.InlineContentHandler; import ca.nrc.cadc.tap.schema.SchemaDesc; import ca.nrc.cadc.tap.schema.TableDesc; -import ca.nrc.cadc.tap.schema.TapSchema; import ca.nrc.cadc.tap.schema.TapSchemaUtil; import ca.nrc.cadc.util.StringUtil; import ca.nrc.cadc.vosi.InvalidTableSetException; import ca.nrc.cadc.vosi.TableReader; -import ca.nrc.cadc.vosi.TableSetReader; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TablesAction.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TablesAction.java index bca65cf4..5601c9ee 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TablesAction.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/TablesAction.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2018. (c) 2018. +* (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 @@ -79,7 +79,6 @@ import ca.nrc.cadc.tap.schema.TapSchemaDAO; import java.io.IOException; import java.security.AccessControlException; - import java.security.Principal; import java.util.Set; import java.util.TreeSet; diff --git a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/Util.java b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/Util.java index 7e2949da..6427047a 100644 --- a/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/Util.java +++ b/cadc-tap-schema/src/main/java/ca/nrc/cadc/vosi/actions/Util.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2018. (c) 2018. +* (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 diff --git a/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java b/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java index cc7e9d32..cef78227 100644 --- a/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java +++ b/youcat/src/intTest/java/org/opencadc/youcat/AbstractTablesTest.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2019. (c) 2019. +* (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 diff --git a/youcat/src/intTest/java/org/opencadc/youcat/CreateTableTest.java b/youcat/src/intTest/java/org/opencadc/youcat/CreateTableTest.java index a8e33565..f01b798a 100644 --- a/youcat/src/intTest/java/org/opencadc/youcat/CreateTableTest.java +++ b/youcat/src/intTest/java/org/opencadc/youcat/CreateTableTest.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2019. (c) 2019. +* (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 diff --git a/youcat/src/intTest/java/org/opencadc/youcat/LoadTableDataTest.java b/youcat/src/intTest/java/org/opencadc/youcat/LoadTableDataTest.java index 89514c3a..bd75e683 100644 --- a/youcat/src/intTest/java/org/opencadc/youcat/LoadTableDataTest.java +++ b/youcat/src/intTest/java/org/opencadc/youcat/LoadTableDataTest.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2018. (c) 2018. +* (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 diff --git a/youcat/src/intTest/java/org/opencadc/youcat/PermissionsTest.java b/youcat/src/intTest/java/org/opencadc/youcat/PermissionsTest.java index b696b7b6..15e7f746 100644 --- a/youcat/src/intTest/java/org/opencadc/youcat/PermissionsTest.java +++ b/youcat/src/intTest/java/org/opencadc/youcat/PermissionsTest.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * -* (c) 2019. (c) 2019. +* (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