diff --git a/api/src/org/labkey/api/reports/report/AbstractReport.java b/api/src/org/labkey/api/reports/report/AbstractReport.java index 9d4d7a35fe9..b1474d336d0 100644 --- a/api/src/org/labkey/api/reports/report/AbstractReport.java +++ b/api/src/org/labkey/api/reports/report/AbstractReport.java @@ -43,6 +43,7 @@ import org.labkey.api.security.SecurityPolicyManager; import org.labkey.api.security.User; import org.labkey.api.security.UserPrincipal; +import org.labkey.api.security.permissions.AdminPermission; import org.labkey.api.security.permissions.DeletePermission; import org.labkey.api.security.permissions.Permission; import org.labkey.api.security.permissions.ReadPermission; @@ -661,7 +662,8 @@ public boolean hasPermission(@NotNull UserPrincipal user, @NotNull Container c, } else { - return !isPrivate() || isOwner(user); + // owners or administrators can access private reports + return !isPrivate() || isOwner(user) || c.hasPermission(user, AdminPermission.class); } } return false; diff --git a/api/src/org/labkey/api/reports/report/ReportDescriptor.java b/api/src/org/labkey/api/reports/report/ReportDescriptor.java index 65be58b2ae9..2ba617776ac 100644 --- a/api/src/org/labkey/api/reports/report/ReportDescriptor.java +++ b/api/src/org/labkey/api/reports/report/ReportDescriptor.java @@ -76,8 +76,7 @@ public class ReportDescriptor extends Entity implements SecurableResource, Clone { public static final String TYPE = "reportDescriptor"; public static final int FLAG_INHERITABLE = 0x01; - - private final static int FLAG_HIDDEN = 0x02; + public static final int FLAG_HIDDEN = 0x02; private String _reportKey; private Integer _owner; diff --git a/api/src/org/labkey/api/reports/report/ReportUrls.java b/api/src/org/labkey/api/reports/report/ReportUrls.java index 612a401220a..efdd6538028 100644 --- a/api/src/org/labkey/api/reports/report/ReportUrls.java +++ b/api/src/org/labkey/api/reports/report/ReportUrls.java @@ -43,10 +43,9 @@ public interface ReportUrls extends UrlProvider ActionURL urlShareReport(Container c, Report r); // Thumbnail or icon, depending on ImageType ActionURL urlImage(Container c, Report r, ThumbnailService.ImageType type, @Nullable Integer revision); - ActionURL urlReportInfo(Container c); ActionURL urlAttachmentReport(Container c, ActionURL returnUrl); ActionURL urlLinkReport(Container c, ActionURL returnUrl); - ActionURL urlReportDetails(Container c, Report r); + ActionURL urlReportDetails(Container c, @Nullable Report r); ActionURL urlQueryReport(Container c, Report r); ActionURL urlManageNotifications(Container c); ActionURL urlModuleThumbnail(Container c); diff --git a/core/src/org/labkey/core/CoreModule.java b/core/src/org/labkey/core/CoreModule.java index 803404e97ce..4b3c80e3610 100644 --- a/core/src/org/labkey/core/CoreModule.java +++ b/core/src/org/labkey/core/CoreModule.java @@ -275,6 +275,7 @@ import org.labkey.core.query.CoreQuerySchema; import org.labkey.core.query.PostgresTableSizesTable; import org.labkey.core.query.PostgresUserSchema; +import org.labkey.core.query.ReportsTable; import org.labkey.core.query.UserAuditProvider; import org.labkey.core.query.UsersDomainKind; import org.labkey.core.reader.DataLoaderServiceImpl; @@ -1464,7 +1465,8 @@ public TabDisplayMode getTabDisplayMode() SqlScriptController.TestCase.class, TableViewFormTestCase.class, UnknownSchemasTest.class, - UserController.TestCase.class + UserController.TestCase.class, + ReportsTable.TestCase.class ); testClasses.addAll(SqlDialectManager.getAllJUnitTests()); diff --git a/core/src/org/labkey/core/query/CoreQuerySchema.java b/core/src/org/labkey/core/query/CoreQuerySchema.java index 888c92a4bf0..a768624584c 100644 --- a/core/src/org/labkey/core/query/CoreQuerySchema.java +++ b/core/src/org/labkey/core/query/CoreQuerySchema.java @@ -107,6 +107,7 @@ public class CoreQuerySchema extends UserSchema public static final String VIEW_CATEGORY_TABLE_NAME = "ViewCategory"; public static final String SHORT_URL_TABLE_NAME = "ShortURL"; public static final String DOCUMENTS_TABLE_NAME = "Documents"; + public static final String REPORTS_TABLE_NAME = "Reports"; public CoreQuerySchema(User user, Container c) { @@ -145,6 +146,9 @@ public Set getTableNames() if (getUser().isTroubleshooter()) names.add(DOCUMENTS_TABLE_NAME); + if (getContainer().hasPermission(getUser(), AdminPermission.class)) + names.add(REPORTS_TABLE_NAME); + if (getUser().hasRootPermission(UserManagementPermission.class)) names.add(API_KEYS_TABLE_NAME); @@ -205,6 +209,8 @@ public TableInfo createTable(String name, ContainerFilter cf) return new ShortUrlTableInfo(this); if (DOCUMENTS_TABLE_NAME.equalsIgnoreCase(name) && getUser().isTroubleshooter()) return new DocumentsTable(this, cf); + if (REPORTS_TABLE_NAME.equalsIgnoreCase(name) && getContainer().hasPermission(getUser(), AdminPermission.class)) + return new ReportsTable(this, cf); return null; } diff --git a/core/src/org/labkey/core/query/ReportsTable.java b/core/src/org/labkey/core/query/ReportsTable.java new file mode 100644 index 00000000000..2a8334d5f36 --- /dev/null +++ b/core/src/org/labkey/core/query/ReportsTable.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2008-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.core.query; + +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.CoreSchema; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DefaultQueryUpdateService; +import org.labkey.api.query.DetailsURL; +import org.labkey.api.query.ExprColumn; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.FilteredTable; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.column.BuiltInColumnTypes; +import org.labkey.api.reports.ReportService; +import org.labkey.api.reports.report.QueryReport; +import org.labkey.api.reports.report.ReportDescriptor; +import org.labkey.api.reports.report.ReportUrls; +import org.labkey.api.security.User; +import org.labkey.api.security.UserPrincipal; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.util.ContainerContext; +import org.labkey.api.util.JunitUtil; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.TestContext; +import org.labkey.api.util.logging.LogHelper; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.UnauthorizedException; +import org.labkey.api.writer.ContainerUser; + +import java.util.List; +import java.util.Map; + +import static org.labkey.api.util.JunitUtil.deleteTestContainer; + +public class ReportsTable extends FilteredTable +{ + public ReportsTable(@NotNull CoreQuerySchema userSchema, ContainerFilter cf) + { + super(CoreSchema.getInstance().getTableInfoReport(), userSchema, cf); + + setName(CoreQuerySchema.REPORTS_TABLE_NAME); + setDescription("Contains a row for each report in the database. Available only to administrators."); + + ReportUrls reportUrls = PageFlowUtil.urlProvider(ReportUrls.class); + ActionURL baseUrl = reportUrls.urlReportDetails(userSchema.getContainer(), null); + DetailsURL detailsURL = new DetailsURL(baseUrl, Map.of(ReportDescriptor.Prop.reportId.toString(), FieldKey.fromParts("RowId"))); + detailsURL.setContainerContext(new ContainerContext.FieldKeyContext(FieldKey.fromParts("ContainerId"))); + setDetailsURL(detailsURL); + + wrapAllColumns(true); + + var folderCol = getMutableColumnOrThrow(FieldKey.fromString("ContainerId")); + folderCol.setLabel("Folder"); + folderCol.setConceptURI(BuiltInColumnTypes.CONTAINERID_CONCEPT_URI); + + getMutableColumnOrThrow("CreatedBy").setConceptURI(BuiltInColumnTypes.USERID_CONCEPT_URI); + getMutableColumnOrThrow("ModifiedBy").setConceptURI(BuiltInColumnTypes.USERID_CONCEPT_URI); + getMutableColumnOrThrow("ReportOwner").setConceptURI(BuiltInColumnTypes.USERID_CONCEPT_URI); + + ColumnInfo flagsCol = getRealTable().getColumn("Flags"); + var hiddenCol = new ExprColumn(this, "Hidden", + new SQLFragment("(CASE WHEN (" + ExprColumn.STR_TABLE_ALIAS + ".Flags & " + ReportDescriptor.FLAG_HIDDEN + ") != 0") + .append(" THEN ").append(getSqlDialect().getBooleanTRUE()) + .append(" ELSE ").append(getSqlDialect().getBooleanFALSE()).append(" END)"), + JdbcType.BOOLEAN, flagsCol); + addColumn(hiddenCol); + + var inheritableCol = new ExprColumn(this, "Inheritable", + new SQLFragment("(CASE WHEN (" + ExprColumn.STR_TABLE_ALIAS + ".Flags & " + ReportDescriptor.FLAG_INHERITABLE + ") != 0") + .append(" THEN ").append(getSqlDialect().getBooleanTRUE()) + .append(" ELSE ").append(getSqlDialect().getBooleanFALSE()).append(" END)"), + JdbcType.BOOLEAN, flagsCol); + addColumn(inheritableCol); + + setDefaultVisibleColumns(List.of( + FieldKey.fromParts("ReportKey"), + FieldKey.fromParts("ContainerId"), + FieldKey.fromParts("Hidden"), + FieldKey.fromParts("Inheritable"), + FieldKey.fromParts("Created"), + FieldKey.fromParts("CreatedBy"), + FieldKey.fromParts("Modified"), + FieldKey.fromParts("ModifiedBy"), + FieldKey.fromParts("ReportOwner") + )); + } + + @Override + protected String getContainerFilterColumn() + { + return "ContainerId"; + } + + @Override + public boolean hasPermission(@NotNull UserPrincipal user, @NotNull Class perm) + { + return (perm.equals(ReadPermission.class) || perm.equals(DeletePermission.class)) && getContainer().hasPermission(user, AdminPermission.class); + } + + @Override + public QueryUpdateService getUpdateService() + { + return new ReportsUpdateService(this, CoreSchema.getInstance().getTableInfoReport()); + } + + protected static class ReportsUpdateService extends DefaultQueryUpdateService + { + public ReportsUpdateService(TableInfo queryTable, TableInfo dbTable) + { + super(queryTable, dbTable); + } + + @Override + protected Map deleteRow(User user, Container container, Map oldRowMap) + { + Integer id = (Integer) oldRowMap.get("rowId"); + Container c = getContainer(oldRowMap); + if (id != null && c != null) + { + var r = ReportService.get().getReport(c, id); + if (r != null) + { + if (r.hasPermission(user, c, DeletePermission.class)) + ReportService.get().deleteReport(ContainerUser.create(c, user), r); + else + throw new UnauthorizedException(String.format("You do not have permission to delete this report from folder : %s", c.getPath())); + } + } + return oldRowMap; + } + + private @Nullable Container getContainer(Map row) + { + String containerId = (String) row.get("containerId"); + if (containerId != null) + return ContainerManager.getForId(containerId); + + return null; + } + } + + public static class TestCase extends Assert + { + private static final Logger LOG = LogHelper.getLogger(ReportsTable.class, "Integration tests for the ReportsTable"); + private static User _user; + private static Container _container; + + @BeforeClass + public static void setup() throws Exception + { + _container = JunitUtil.getTestContainer(); + _user = TestContext.get().getUser(); + } + + @AfterClass + public static void cleanup() + { + deleteTestContainer(); + _container = null; + _user = null; + } + + @Test + public void testReportsTableAdminOnlyAccess() + { + LOG.info("Validate Core.Reports is admin only"); + + var schema = QueryService.get().getUserSchema(User.getAdminServiceUser(), _container, CoreQuerySchema.NAME); + assertNotNull("Expected admin access to the " + CoreQuerySchema.REPORTS_TABLE_NAME + " table", schema.getTable(CoreQuerySchema.REPORTS_TABLE_NAME)); + + schema = QueryService.get().getUserSchema(User.getSearchUser(), _container, CoreQuerySchema.NAME); + assertNull("Expected admin access to the " + CoreQuerySchema.REPORTS_TABLE_NAME + " table", schema.getTable(CoreQuerySchema.REPORTS_TABLE_NAME)); + } + + private QueryUpdateService ensureUpdateService(String tableName) + { + var schema = QueryService.get().getUserSchema(_user, _container, CoreQuerySchema.NAME); + var table = schema.getTable(tableName); + var qus = table.getUpdateService(); + assertNotNull("Expected update service for " + tableName, qus); + + return qus; + } + + @Test + public void testReportsApiAccess() throws Exception + { + var qus = ensureUpdateService(CoreQuerySchema.REPORTS_TABLE_NAME); + + try + { + BatchValidationException errors = new BatchValidationException(); + Map row = CaseInsensitiveHashMap.of( + "reportKey", "foo/bar", + "hidden", true + ); + qus.insertRows(_user, _container, List.of(row), errors, null, null); + fail("Insert should have thrown UnauthorizedException"); + } + catch (UnauthorizedException e) + { + // expected + } + + // Save a report through the service + var queryReport = ReportService.get().createReportInstance(QueryReport.TYPE); + var descriptor = queryReport.getDescriptor(); + descriptor.setReportName("custom query report"); + + var identifier = ReportService.get().saveReportEx(ContainerUser.create(_container, _user), "reportKey", queryReport, true); + assertTrue("Unable to save a query report", identifier.getRowId() != 0); + + var savedReport = identifier.getReport(ContainerUser.create(_container, _user)); + assertNotNull("Unable to retrieve a saved report", savedReport); + + BatchValidationException errors = new BatchValidationException(); + Map row = CaseInsensitiveHashMap.of( + "rowId", savedReport.getDescriptor().getReportId().getRowId(), + "flags", 2 + ); + + try + { + qus.updateRows(_user, _container, List.of(row), null, errors, null, null); + fail("Update should have thrown UnauthorizedException"); + } + catch (UnauthorizedException e) + { + // expected, delete the query + qus.deleteRows(_user, _container, List.of(row), null, null); + } + } + } +} diff --git a/query/src/org/labkey/query/QueryModule.java b/query/src/org/labkey/query/QueryModule.java index 0e57baef4de..7195986c513 100644 --- a/query/src/org/labkey/query/QueryModule.java +++ b/query/src/org/labkey/query/QueryModule.java @@ -358,6 +358,7 @@ public void doStartup(ModuleContext moduleContext) ); McpService.get().register(new QueryMcp()); + QueryUserSchema.register(this); } @Override @@ -432,7 +433,8 @@ public Set getSchemaNames() Query.TestCase.class, ReportsController.SerializationTest.class, SqlParser.SqlParserTestCase.class, - TableWriter.TestCase.class + TableWriter.TestCase.class, + QueryUserSchema.TestCase.class ); } diff --git a/query/src/org/labkey/query/QueryUserSchema.java b/query/src/org/labkey/query/QueryUserSchema.java new file mode 100644 index 00000000000..09abd608a81 --- /dev/null +++ b/query/src/org/labkey/query/QueryUserSchema.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2008-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.query; + +import org.apache.logging.log4j.Logger; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.TableInfo; +import org.labkey.api.module.Module; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DefaultSchema; +import org.labkey.api.query.QuerySchema; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.SchemaKey; +import org.labkey.api.query.UserSchema; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.util.JunitUtil; +import org.labkey.api.util.TestContext; +import org.labkey.api.util.logging.LogHelper; +import org.labkey.api.view.UnauthorizedException; +import org.labkey.query.query.CustomViewsTable; +import org.labkey.query.query.QueriesTable; +import org.labkey.query.query.QueryDbSchema; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.labkey.api.util.JunitUtil.deleteTestContainer; + +public class QueryUserSchema extends UserSchema +{ + public static final String SCHEMA_NAME = "query"; + public static final String SCHEMA_DESCR = "Contains query related data."; + + public static final String CUSTOM_VIEWS_TABLE_NAME = "CustomViews"; + public static final String QUERIES_TABLE_NAME = "Queries"; + + static public void register(final Module module) + { + DefaultSchema.registerProvider(SCHEMA_NAME, new DefaultSchema.SchemaProvider(module) + { + @Override + public QuerySchema createSchema(DefaultSchema schema, Module module) + { + return new QueryUserSchema(schema.getUser(), schema.getContainer()); + } + + @Override + public boolean isAvailable(DefaultSchema schema, Module module) + { + return schema.getContainer().hasPermission(schema.getUser(), AdminPermission.class); + } + }); + } + + public QueryUserSchema(User user, Container container) + { + super(SCHEMA_NAME, SCHEMA_DESCR, user, container, QueryDbSchema.getInstance().getSchema()); + } + + @Override + public Set getTableNames() + { + Set names = new HashSet<>(); + + if (getContainer().hasPermission(getUser(), AdminPermission.class)) + { + names.add(CUSTOM_VIEWS_TABLE_NAME); + names.add(QUERIES_TABLE_NAME); + } + + return names; + } + + @Override + public TableInfo createTable(String name, ContainerFilter cf) + { + if (getContainer().hasPermission(getUser(), AdminPermission.class)) + { + if (CUSTOM_VIEWS_TABLE_NAME.equalsIgnoreCase(name)) + return new CustomViewsTable(this, cf); + if (QUERIES_TABLE_NAME.equalsIgnoreCase(name)) + return new QueriesTable(this, cf); + } + + return null; + } + + public static class TestCase extends Assert + { + private static final Logger LOG = LogHelper.getLogger(QueryUserSchema.class, "Integration tests for the Query user schema"); + private static User _user; + private static Container _container; + + @BeforeClass + public static void setup() throws Exception + { + _container = JunitUtil.getTestContainer(); + _user = TestContext.get().getUser(); + } + + @AfterClass + public static void cleanup() + { + deleteTestContainer(); + _container = null; + _user = null; + } + + @Test + public void testCustomViewsAdminOnlyAccess() + { + LOG.info("Validate Query.CustomViews is admin only"); + + var schema = QueryService.get().getUserSchema(User.getAdminServiceUser(), _container, SCHEMA_NAME); + assertNotNull("Expected admin access to the " + CUSTOM_VIEWS_TABLE_NAME + " table", schema.getTable(CUSTOM_VIEWS_TABLE_NAME)); + + schema = QueryService.get().getUserSchema(User.getSearchUser(), _container, SCHEMA_NAME); + assertNull("Expected no reader access to the " + SCHEMA_NAME + " schema", schema); + } + + @Test + public void testCustomViewsApiAccess() throws Exception + { + var qus = ensureUpdateService(CUSTOM_VIEWS_TABLE_NAME); + + BatchValidationException errors = new BatchValidationException(); + Map row = CaseInsensitiveHashMap.of( + "schema", "test", + "queryName", "query", + "flags", 0 + ); + List> views = qus.insertRows(_user, _container, List.of(row), errors, null, null); + assertFalse("Unexpected error on insert", errors.hasErrors()); + + Map newView = views.getFirst(); + newView.put("flags", 3); + qus.updateRows(_user, _container, List.of(newView), null, errors, null, null); + assertFalse("Unexpected error on update", errors.hasErrors()); + + // finally, delete the custom view + qus.deleteRows(_user, _container, List.of(newView), null, null); + } + + @Test + public void testQueriesAdminOnlyAccess() + { + LOG.info("Validate Query.Queries is admin only"); + + var schema = QueryService.get().getUserSchema(User.getAdminServiceUser(), _container, SCHEMA_NAME); + assertNotNull("Expected admin access to the " + QUERIES_TABLE_NAME + " table", schema.getTable(QUERIES_TABLE_NAME)); + + // admin only access to the schema is tested in testCustomViewsAdminOnlyAccess + } + + private QueryUpdateService ensureUpdateService(String tableName) + { + var schema = QueryService.get().getUserSchema(_user, _container, SCHEMA_NAME); + var table = schema.getTable(tableName); + var qus = table.getUpdateService(); + assertNotNull("Expected update service for " + tableName, qus); + + return qus; + } + + @Test + public void testQueriesApiAccess() throws Exception + { + var qus = ensureUpdateService(QUERIES_TABLE_NAME); + + try + { + BatchValidationException errors = new BatchValidationException(); + Map row = CaseInsensitiveHashMap.of( + "schema", "test", + "name", "custom query", + "sql", "SELECT * FROM test" + ); + qus.insertRows(_user, _container, List.of(row), errors, null, null); + fail("Insert should have thrown UnauthorizedException"); + } + catch (UnauthorizedException e) + { + // expected + } + + String customQueryName = "custom query"; + var queryDef = QueryService.get().createQueryDef(_user, _container, SchemaKey.fromParts("lists"), customQueryName); + queryDef.setSql("SELECT * FROM lists"); + queryDef.save(_user, _container, false); + + queryDef = QueryService.get().getQueryDef(_user, _container, "lists", customQueryName); + assertNotNull("Unable to retrieve a saved query def", queryDef); + + if (queryDef instanceof QueryDefinitionImpl queryImpl) + { + BatchValidationException errors = new BatchValidationException(); + Map row = CaseInsensitiveHashMap.of( + "queryDefId", queryImpl.getQueryDef().getQueryDefId(), + "name", "custom query", + "sql", "SELECT * FROM test" + ); + + try + { + qus.updateRows(_user, _container, List.of(row), null, errors, null, null); + fail("Update should have thrown UnauthorizedException"); + } + catch (UnauthorizedException e) + { + // expected, delete the query + qus.deleteRows(_user, _container, List.of(row), null, null); + } + } + else + Assert.fail("Unexpected query def type: " + queryDef.getClass().getName()); + } + } +} diff --git a/query/src/org/labkey/query/controllers/InternalNewViewForm.java b/query/src/org/labkey/query/controllers/InternalNewViewForm.java deleted file mode 100644 index 7fcb8089c6e..00000000000 --- a/query/src/org/labkey/query/controllers/InternalNewViewForm.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2008-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.labkey.query.controllers; - -public class InternalNewViewForm -{ - public String ff_schemaName; - public String ff_queryName; - public String ff_viewName; - public boolean ff_share; - public boolean ff_inherit; - - public void setFf_schemaName(String name) - { - ff_schemaName = name; - } - - public void setFf_queryName(String name) - { - ff_queryName = name; - } - - public void setFf_viewName(String name) - { - ff_viewName = name; - } - - public void setFf_share(boolean share) - { - ff_share = share; - } - - public void setFf_inherit(boolean inherit) - { - ff_inherit = inherit; - } -} diff --git a/query/src/org/labkey/query/controllers/InternalSourceViewForm.java b/query/src/org/labkey/query/controllers/InternalSourceViewForm.java deleted file mode 100644 index 6515d25d4aa..00000000000 --- a/query/src/org/labkey/query/controllers/InternalSourceViewForm.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2008-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.labkey.query.controllers; - -public class InternalSourceViewForm extends InternalViewForm -{ - public Boolean ff_inherit; - public Boolean ff_hidden; - public String ff_columnList; - public String ff_filter; - - public Boolean getFf_inherit() - { - return ff_inherit; - } - - public void setFf_inherit(Boolean ff_inherit) - { - this.ff_inherit = ff_inherit; - } - - public Boolean getFf_hidden() - { - return ff_hidden; - } - - public void setFf_hidden(Boolean ff_hidden) - { - this.ff_hidden = ff_hidden; - } - - public void setFf_columnList(String str) - { - ff_columnList = str; - } - - public void setFf_filter(String str) - { - ff_filter = str; - } -} diff --git a/query/src/org/labkey/query/controllers/InternalViewForm.java b/query/src/org/labkey/query/controllers/InternalViewForm.java deleted file mode 100644 index 0a8612e3b96..00000000000 --- a/query/src/org/labkey/query/controllers/InternalViewForm.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2008-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.labkey.query.controllers; - -import org.labkey.api.security.permissions.EditSharedViewPermission; -import org.labkey.api.view.NotFoundException; -import org.labkey.api.view.UnauthorizedException; -import org.labkey.api.view.ViewContext; -import org.labkey.api.view.ViewForm; -import org.labkey.query.persist.CstmView; -import org.labkey.query.persist.QueryManager; - -public class InternalViewForm extends ViewForm -{ - private int _customViewId; - private CstmView _view; - - public CstmView getViewAndCheckPermission() - { - if (_view != null) - return _view; - QueryManager mgr = QueryManager.get(); - CstmView view = mgr.getCustomView(getContainer(), _customViewId); - checkEdit(getViewContext(), view); - _view = view; - return _view; - } - - public int getCustomViewId() - { - return _customViewId; - } - - public void setCustomViewId(int id) - { - _customViewId = id; - } - - public static void checkEdit(ViewContext context, CstmView view) - { - if (view == null) - { - throw new NotFoundException(); - } - if (!view.getContainerId().equals(context.getContainer().getId())) - { - throw new UnauthorizedException(); - } - if (view.getCustomViewOwner() == null) - { - if (!context.hasPermission(EditSharedViewPermission.class)) - { - throw new UnauthorizedException(); - } - } - else - { - // must be owner or site admin - if (!context.getUser().hasSiteAdminPermission() && view.getCustomViewOwner().intValue() != context.getUser().getUserId()) - { - throw new UnauthorizedException(); - } - } - } -} diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 14b4cfa9bce..427e550af11 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -90,7 +90,6 @@ import org.labkey.api.collections.CaseInsensitiveHashMap; import org.labkey.api.collections.CaseInsensitiveHashSet; import org.labkey.api.collections.IntHashMap; -import org.labkey.api.collections.LabKeyCollectors; import org.labkey.api.collections.RowMapFactory; import org.labkey.api.collections.Sets; import org.labkey.api.data.AbstractTableInfo; @@ -293,7 +292,6 @@ import org.labkey.query.audit.QueryUpdateAuditProvider; import org.labkey.query.model.MetadataTableJSONMixin; import org.labkey.query.persist.AbstractExternalSchemaDef; -import org.labkey.query.persist.CstmView; import org.labkey.query.persist.ExternalSchemaDef; import org.labkey.query.persist.ExternalSchemaDefCache; import org.labkey.query.persist.LinkedSchemaDef; @@ -6325,203 +6323,6 @@ else if (!existingView.isEditable()) } } - /** Minimalist, secret UI to help users recover if they've created a broken view somehow */ - @RequiresPermission(AdminPermission.class) - public class ManageViewsAction extends SimpleViewAction - { - @SuppressWarnings("UnusedDeclaration") - public ManageViewsAction() - { - } - - public ManageViewsAction(ViewContext ctx) - { - setViewContext(ctx); - } - - @Override - public ModelAndView getView(QueryForm form, BindException errors) - { - return new JspView<>("/org/labkey/query/view/manageViews.jsp", form, errors); - } - - @Override - public void addNavTrail(NavTree root) - { - new BeginAction(getViewContext()).addNavTrail(root); - root.addChild("Manage Views", QueryController.this.getViewContext().getActionURL()); - } - } - - - /** Minimalist, secret UI to help users recover if they've created a broken view somehow */ - @RequiresPermission(AdminPermission.class) - public class InternalDeleteView extends ConfirmAction - { - @Override - public ModelAndView getConfirmView(InternalViewForm form, BindException errors) - { - return new JspView<>("/org/labkey/query/view/internalDeleteView.jsp", form, errors); - } - - @Override - public boolean handlePost(InternalViewForm form, BindException errors) - { - CstmView view = form.getViewAndCheckPermission(); - QueryManager.get().delete(getUser(), view); - return true; - } - - @Override - public void validateCommand(InternalViewForm internalViewForm, Errors errors) - { - } - - @Override - @NotNull - public ActionURL getSuccessURL(InternalViewForm internalViewForm) - { - return new ActionURL(ManageViewsAction.class, getContainer()); - } - } - - /** Minimalist, secret UI to help users recover if they've created a broken view somehow */ - @RequiresPermission(AdminPermission.class) - public class InternalSourceViewAction extends FormViewAction - { - @Override - public void validateCommand(InternalSourceViewForm target, Errors errors) - { - } - - @Override - public ModelAndView getView(InternalSourceViewForm form, boolean reshow, BindException errors) - { - CstmView view = form.getViewAndCheckPermission(); - form.ff_inherit = QueryManager.get().canInherit(view.getFlags()); - form.ff_hidden = QueryManager.get().isHidden(view.getFlags()); - form.ff_columnList = view.getColumns(); - form.ff_filter = view.getFilter(); - return new JspView<>("/org/labkey/query/view/internalSourceView.jsp", form, errors); - } - - @Override - public boolean handlePost(InternalSourceViewForm form, BindException errors) - { - CstmView view = form.getViewAndCheckPermission(); - int flags = view.getFlags(); - flags = QueryManager.get().setCanInherit(flags, form.ff_inherit); - flags = QueryManager.get().setIsHidden(flags, form.ff_hidden); - view.setFlags(flags); - view.setColumns(form.ff_columnList); - view.setFilter(form.ff_filter); - QueryManager.get().update(getUser(), view); - return true; - } - - @Override - public ActionURL getSuccessURL(InternalSourceViewForm form) - { - return new ActionURL(ManageViewsAction.class, getContainer()); - } - - @Override - public void addNavTrail(NavTree root) - { - new ManageViewsAction(getViewContext()).addNavTrail(root); - root.addChild("Edit source of Grid View"); - } - } - - /** Minimalist, secret UI to help users recover if they've created a broken view somehow */ - @RequiresPermission(AdminPermission.class) - public class InternalNewViewAction extends FormViewAction - { - int _customViewId = 0; - - @Override - public void validateCommand(InternalNewViewForm form, Errors errors) - { - if (StringUtils.trimToNull(form.ff_schemaName) == null) - { - errors.reject(ERROR_MSG, "Schema name cannot be blank."); - } - if (StringUtils.trimToNull(form.ff_queryName) == null) - { - errors.reject(ERROR_MSG, "Query name cannot be blank"); - } - } - - @Override - public ModelAndView getView(InternalNewViewForm form, boolean reshow, BindException errors) - { - return new JspView<>("/org/labkey/query/view/internalNewView.jsp", form, errors); - } - - @Override - public boolean handlePost(InternalNewViewForm form, BindException errors) - { - if (form.ff_share) - { - if (!getContainer().hasPermission(getUser(), AdminPermission.class)) - throw new UnauthorizedException(); - } - List existing = QueryManager.get().getCstmViews(getContainer(), form.ff_schemaName, form.ff_queryName, form.ff_viewName, form.ff_share ? null : getUser(), false, false); - CstmView view; - if (!existing.isEmpty()) - { - } - else - { - view = new CstmView(); - view.setSchema(form.ff_schemaName); - view.setQueryName(form.ff_queryName); - view.setName(form.ff_viewName); - view.setContainerId(getContainer().getId()); - if (form.ff_share) - { - view.setCustomViewOwner(null); - } - else - { - view.setCustomViewOwner(getUser().getUserId()); - } - if (form.ff_inherit) - { - view.setFlags(QueryManager.get().setCanInherit(view.getFlags(), form.ff_inherit)); - } - InternalViewForm.checkEdit(getViewContext(), view); - try - { - view = QueryManager.get().insert(getUser(), view); - } - catch (Exception e) - { - LogManager.getLogger(QueryController.class).error("Error", e); - errors.reject(ERROR_MSG, "An exception occurred: " + e); - return false; - } - _customViewId = view.getCustomViewId(); - } - return true; - } - - @Override - public ActionURL getSuccessURL(InternalNewViewForm form) - { - ActionURL forward = new ActionURL(InternalSourceViewAction.class, getContainer()); - forward.addParameter("customViewId", Integer.toString(_customViewId)); - return forward; - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Create New Grid View"); - } - } - - @ActionNames("clearSelected, selectNone") @RequiresPermission(ReadPermission.class) @Action(ActionType.SelectData.class) @@ -8599,10 +8400,6 @@ controller.new NewQueryAction(), new ManageRemoteConnectionsAction(), new ReloadExternalSchemaAction(), new ReloadAllUserSchemas(), - controller.new ManageViewsAction(), - controller.new InternalDeleteView(), - controller.new InternalSourceViewAction(), - controller.new InternalNewViewAction(), new QueryExportAuditRedirectAction() ); diff --git a/query/src/org/labkey/query/persist/CstmView.java b/query/src/org/labkey/query/persist/CstmView.java index af0f7512486..f1ef819b168 100644 --- a/query/src/org/labkey/query/persist/CstmView.java +++ b/query/src/org/labkey/query/persist/CstmView.java @@ -16,12 +16,14 @@ package org.labkey.query.persist; +import org.labkey.api.collections.CaseInsensitiveHashMap; import org.labkey.api.data.BeanObjectFactory; import org.labkey.api.data.Entity; import org.labkey.api.data.ObjectFactory; import org.labkey.api.util.UnexpectedException; import java.io.Serializable; +import java.util.Map; public final class CstmView extends Entity implements Cloneable, Serializable { @@ -139,6 +141,27 @@ public CstmView clone() } } + public static Map toRow(CstmView view) + { + Map row = new CaseInsensitiveHashMap<>(); + row.put("customViewId", view.getCustomViewId()); + row.put("entityId", view.getEntityId()); + row.put("schema", view.getSchema()); + row.put("queryName", view.getQueryName()); + row.put("name", view.getName()); + row.put("customViewOwner", view.getCustomViewOwner()); + row.put("container", view.getContainerId()); + row.put("columns", view.getColumns()); + row.put("filter", view.getFilter()); + row.put("flags", view.getFlags()); + row.put("created", view.getCreated()); + row.put("createdBy", view.getCreatedBy()); + row.put("modified", view.getModified()); + row.put("modifiedBy", view.getModifiedBy()); + + return row; + } + static { ObjectFactory.Registry.register(CstmView.class, new CstmViewObjectFactory()); diff --git a/query/src/org/labkey/query/query/CustomViewsTable.java b/query/src/org/labkey/query/query/CustomViewsTable.java new file mode 100644 index 00000000000..0e1f7b46ddc --- /dev/null +++ b/query/src/org/labkey/query/query/CustomViewsTable.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2008-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.query.query; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.DataColumn; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.RenderContext; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DefaultQueryUpdateService; +import org.labkey.api.query.ExprColumn; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.FilteredTable; +import org.labkey.api.query.InvalidKeyException; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; +import org.labkey.api.query.ValidationException; +import org.labkey.api.query.column.BuiltInColumnTypes; +import org.labkey.api.security.User; +import org.labkey.api.security.UserPrincipal; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.security.permissions.EditSharedViewPermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.util.HtmlString; +import org.labkey.api.view.NotFoundException; +import org.labkey.api.view.UnauthorizedException; +import org.labkey.query.QueryUserSchema; +import org.labkey.query.persist.CstmView; +import org.labkey.query.persist.QueryManager; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CustomViewsTable extends FilteredTable +{ + public CustomViewsTable(@NotNull QueryUserSchema userSchema, ContainerFilter cf) + { + super(QueryDbSchema.getInstance().getTableInfoCustomView(), userSchema, cf); + + setName(QueryUserSchema.CUSTOM_VIEWS_TABLE_NAME); + setDescription("Contains a row for each saved custom view. Available only to administrators."); + + setImportURL(LINK_DISABLER); + wrapAllColumns(true); + var customViewIdCol = getMutableColumnOrThrow("CustomViewId"); + customViewIdCol.setKeyField(true); + + getMutableColumnOrThrow("EntityId"); + getMutableColumnOrThrow("Schema").setLabel("Schema Name"); + getMutableColumnOrThrow("Name").setLabel("View Name"); + + var ownerCol = getMutableColumnOrThrow("CustomViewOwner"); + ownerCol.setLabel("Owner"); + ownerCol.setConceptURI(BuiltInColumnTypes.USERID_CONCEPT_URI); + + var folderCol = getMutableColumnOrThrow(FieldKey.fromString("Container")); + folderCol.setLabel("Folder"); + folderCol.setConceptURI(BuiltInColumnTypes.CONTAINERID_CONCEPT_URI); + + getMutableColumnOrThrow("CreatedBy").setConceptURI(BuiltInColumnTypes.USERID_CONCEPT_URI); + getMutableColumnOrThrow("ModifiedBy").setConceptURI(BuiltInColumnTypes.USERID_CONCEPT_URI); + getMutableColumnOrThrow("Columns"); + getMutableColumnOrThrow("Filter"); + getMutableColumnOrThrow("Flags").setDisplayColumnFactory(FlagDisplayColumn::new); + + ColumnInfo flagsCol = getRealTable().getColumn("Flags"); + var hiddenCol = new ExprColumn(this, "Hidden", + new SQLFragment("(CASE WHEN (" + ExprColumn.STR_TABLE_ALIAS + ".Flags & " + QueryManager.FLAG_HIDDEN + ") != 0") + .append(" THEN ").append(getSqlDialect().getBooleanTRUE()) + .append(" ELSE ").append(getSqlDialect().getBooleanFALSE()).append(" END)"), + JdbcType.BOOLEAN, flagsCol); + addColumn(hiddenCol); + + var inheritableCol = new ExprColumn(this, "Inheritable", + new SQLFragment("(CASE WHEN (" + ExprColumn.STR_TABLE_ALIAS + ".Flags & " + QueryManager.FLAG_INHERITABLE + ") != 0") + .append(" THEN ").append(getSqlDialect().getBooleanTRUE()) + .append(" ELSE ").append(getSqlDialect().getBooleanFALSE()).append(" END)"), + JdbcType.BOOLEAN, flagsCol); + addColumn(inheritableCol); + + setDefaultVisibleColumns(List.of( + FieldKey.fromParts("Schema"), + FieldKey.fromParts("QueryName"), + FieldKey.fromParts("Name"), + FieldKey.fromParts("CustomViewOwner"), + FieldKey.fromParts("Container"), + FieldKey.fromParts("Hidden"), + FieldKey.fromParts("Inheritable"), + FieldKey.fromParts("Created"), + FieldKey.fromParts("CreatedBy"), + FieldKey.fromParts("Modified"), + FieldKey.fromParts("ModifiedBy") + )); + } + + @Override + public boolean hasPermission(@NotNull UserPrincipal user, @NotNull Class perm) + { + return getContainer().hasPermission(user, AdminPermission.class); + } + + @Override + public QueryUpdateService getUpdateService() + { + return new CustomViewsUpdateService(this, QueryDbSchema.getInstance().getTableInfoCustomView()); + } + + public static class FlagDisplayColumn extends DataColumn + { + public FlagDisplayColumn(ColumnInfo col) + { + super(col); + } + + @NotNull + @Override + public HtmlString getFormattedHtml(RenderContext ctx) + { + var value = getValue(ctx); + if (value instanceof Integer flag) + { + List flags = new ArrayList<>(); + if ((flag & QueryManager.FLAG_INHERITABLE) != 0) + flags.add("inherit"); + if ((flag & QueryManager.FLAG_HIDDEN) != 0) + flags.add("hidden"); + + return HtmlString.of(String.join(", ", flags)); + } + return super.getFormattedHtml(ctx); + } + } + + protected static class CustomViewsUpdateService extends DefaultQueryUpdateService + { + public CustomViewsUpdateService(TableInfo queryTable, TableInfo dbTable) + { + super(queryTable, dbTable); + } + + @Override + protected Map deleteRow(User user, Container container, Map oldRow) throws QueryUpdateServiceException, SQLException, InvalidKeyException + { + Integer id = (Integer)oldRow.get("customViewId"); + Container c = getContainer(oldRow); + if (id != null && c != null) + { + var view = QueryManager.get().getCustomView(c, id); + if (view != null) + { + if (view.getCustomViewOwner() == null) + { + if (!c.hasPermission(user, EditSharedViewPermission.class)) + throw new UnauthorizedException(); + } + else + { + // must be owner or site admin + if (!user.hasSiteAdminPermission() && view.getCustomViewOwner().intValue() != user.getUserId()) + throw new UnauthorizedException(); + } + QueryManager.get().delete(user, view); + } + } + return oldRow; + } + + @Override + public List> insertRows(User user, Container container, List> rows, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) + { + List> result = new ArrayList<>(); + for (Map row : rows) + { + CaseInsensitiveHashMap rowMap = new CaseInsensitiveHashMap<>(row); + CstmView view = new CstmView(); + view.setContainerId(container.getId()); + view.setSchema((String) rowMap.get("schema")); + view.setQueryName((String) rowMap.get("queryName")); + view.setName((String) rowMap.get("name")); + if (rowMap.containsKey("customViewOwner")) + view.setCustomViewOwner((Integer) rowMap.get("customViewOwner")); + if (rowMap.containsKey("columns")) + view.setColumns((String) rowMap.get("columns")); + if (rowMap.containsKey("filter")) + view.setFilter((String) rowMap.get("filter")); + if (rowMap.get("flags") instanceof Integer flags) + view.setFlags(flags); + + validate(view, container, user); + result.add(CstmView.toRow(QueryManager.get().insert(user, view))); + } + return result; + } + + @Override + protected Map _update(User user, Container container, Map row, Map oldRow, Object[] keys) throws SQLException, ValidationException + { + Integer id = (Integer) oldRow.get("customViewId"); + Container c = getContainer(oldRow); + if (id != null && c != null) + { + CstmView view = QueryManager.get().getCustomView(c, id); + if (view == null) + throw new ValidationException("Custom view not found: " + id); + if (row.containsKey("schema")) + view.setSchema((String) row.get("schema")); + if (row.containsKey("queryName")) + view.setQueryName((String) row.get("queryName")); + if (row.containsKey("name")) + view.setName((String) row.get("name")); + if (row.containsKey("customViewOwner")) + view.setCustomViewOwner((Integer) row.get("customViewOwner")); + if (row.containsKey("columns")) + view.setColumns((String) row.get("columns")); + if (row.containsKey("filter")) + view.setFilter((String) row.get("filter")); + if (row.containsKey("flags") && row.get("flags") instanceof Integer flags) + view.setFlags(flags); + + validate(view, c, user); + return CstmView.toRow(QueryManager.get().update(user, view)); + } + return oldRow; + } + + private @Nullable Container getContainer(Map row) + { + String containerId = (String) row.get("container"); + if (containerId != null) + return ContainerManager.getForId(containerId); + + return null; + } + + private void validate(CstmView view, Container c, User user) + { + if (view == null) + { + throw new NotFoundException(); + } + if (!view.getContainerId().equals(c.getId())) + { + throw new UnauthorizedException(); + } + if (view.getCustomViewOwner() == null) + { + if (!c.hasPermission(user, EditSharedViewPermission.class)) + { + throw new UnauthorizedException(); + } + } + else + { + // must be owner or site admin + if (!user.hasSiteAdminPermission() && view.getCustomViewOwner().intValue() != user.getUserId()) + { + throw new UnauthorizedException(); + } + } + } + } +} diff --git a/query/src/org/labkey/query/query/QueriesTable.java b/query/src/org/labkey/query/query/QueriesTable.java new file mode 100644 index 00000000000..f397cb307e3 --- /dev/null +++ b/query/src/org/labkey/query/query/QueriesTable.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2008-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.query.query; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.DefaultQueryUpdateService; +import org.labkey.api.query.ExprColumn; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.FilteredTable; +import org.labkey.api.query.InvalidKeyException; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; +import org.labkey.api.query.column.BuiltInColumnTypes; +import org.labkey.api.security.User; +import org.labkey.api.security.UserPrincipal; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.query.CustomQueryDefinitionImpl; +import org.labkey.query.QueryDefinitionImpl; +import org.labkey.query.QueryUserSchema; +import org.labkey.query.persist.QueryDefCache; +import org.labkey.query.persist.QueryManager; + +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +public class QueriesTable extends FilteredTable +{ + public QueriesTable(@NotNull QueryUserSchema userSchema, ContainerFilter cf) + { + super(QueryDbSchema.getInstance().getTableInfoQueryDef(), userSchema, cf); + + setName(QueryUserSchema.QUERIES_TABLE_NAME); + setDescription("Contains a row for each query (or metadata) in the database. Available only to administrators."); + + setImportURL(LINK_DISABLER); + wrapAllColumns(true); + + getMutableColumnOrThrow("QueryDefId").setKeyField(true); + var folderCol = getMutableColumnOrThrow(FieldKey.fromString("Container")); + folderCol.setLabel("Folder"); + folderCol.setConceptURI(BuiltInColumnTypes.CONTAINERID_CONCEPT_URI); + + getMutableColumnOrThrow("CreatedBy").setConceptURI(BuiltInColumnTypes.USERID_CONCEPT_URI); + getMutableColumnOrThrow("ModifiedBy").setConceptURI(BuiltInColumnTypes.USERID_CONCEPT_URI); + getMutableColumnOrThrow("Flags").setDisplayColumnFactory(CustomViewsTable.FlagDisplayColumn::new); + + var flagsCol = getRealTable().getColumn("Flags"); + var hiddenCol = new ExprColumn(this, "Hidden", + new SQLFragment("(CASE WHEN (" + ExprColumn.STR_TABLE_ALIAS + ".Flags & " + QueryManager.FLAG_HIDDEN + ") != 0") + .append(" THEN ").append(getSqlDialect().getBooleanTRUE()) + .append(" ELSE ").append(getSqlDialect().getBooleanFALSE()).append(" END)"), + JdbcType.BOOLEAN, flagsCol); + addColumn(hiddenCol); + + var inheritableCol = new ExprColumn(this, "Inheritable", + new SQLFragment("(CASE WHEN (" + ExprColumn.STR_TABLE_ALIAS + ".Flags & " + QueryManager.FLAG_INHERITABLE + ") != 0") + .append(" THEN ").append(getSqlDialect().getBooleanTRUE()) + .append(" ELSE ").append(getSqlDialect().getBooleanFALSE()).append(" END)"), + JdbcType.BOOLEAN, flagsCol); + addColumn(inheritableCol); + + setDefaultVisibleColumns(List.of( + FieldKey.fromParts("Schema"), + FieldKey.fromParts("Name"), + FieldKey.fromParts("Description"), + FieldKey.fromParts("Container"), + FieldKey.fromParts("Hidden"), + FieldKey.fromParts("Inheritable"), + FieldKey.fromParts("Created"), + FieldKey.fromParts("CreatedBy"), + FieldKey.fromParts("Modified"), + FieldKey.fromParts("ModifiedBy") + )); + } + + @Override + public boolean hasPermission(@NotNull UserPrincipal user, @NotNull Class perm) + { + return (perm.equals(ReadPermission.class) || perm.equals(DeletePermission.class)) && getContainer().hasPermission(user, AdminPermission.class); + } + + @Override + public QueryUpdateService getUpdateService() + { + return new QueriesUpdateService(this, QueryDbSchema.getInstance().getTableInfoQueryDef()); + } + + protected static class QueriesUpdateService extends DefaultQueryUpdateService + { + public QueriesUpdateService(TableInfo queryTable, TableInfo dbTable) + { + super(queryTable, dbTable); + } + + @Override + protected Map deleteRow(User user, Container container, Map oldRowMap) throws SQLException, QueryUpdateServiceException, InvalidKeyException + { + Container c = getContainer(oldRowMap); + if (c != null) + { + var queryDef = QueryDefCache.getQueryDefById(c, (Integer)oldRowMap.get("queryDefId")); + if (queryDef != null) + { + QueryDefinitionImpl queryDefImpl = new CustomQueryDefinitionImpl(user, c, queryDef); + queryDefImpl.delete(user); + } + } + return oldRowMap; + } + + private @Nullable Container getContainer(Map row) + { + String containerId = (String) row.get("container"); + if (containerId != null) + return ContainerManager.getForId(containerId); + + return null; + } + } +} diff --git a/query/src/org/labkey/query/query/QueryDbSchema.java b/query/src/org/labkey/query/query/QueryDbSchema.java new file mode 100644 index 00000000000..23815336f42 --- /dev/null +++ b/query/src/org/labkey/query/query/QueryDbSchema.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2008-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.query.query; + +import org.labkey.api.data.DbSchema; +import org.labkey.api.data.DbSchemaType; +import org.labkey.api.data.TableInfo; + +public class QueryDbSchema +{ + private static final QueryDbSchema instance = new QueryDbSchema(); + private static final String SCHEMA_NAME = "query"; + + public static QueryDbSchema getInstance() + { + return instance; + } + + private QueryDbSchema() + { + } + + public DbSchema getSchema() + { + return DbSchema.get(SCHEMA_NAME, DbSchemaType.Module); + } + + public String getSchemaName() + { + return SCHEMA_NAME; + } + + public TableInfo getTableInfoCustomView() + { + return getSchema().getTable("CustomView"); + } + + public TableInfo getTableInfoQueryDef() + { + return getSchema().getTable("QueryDef"); + } +} diff --git a/query/src/org/labkey/query/reports/ReportsController.java b/query/src/org/labkey/query/reports/ReportsController.java index 4be4cb7f4c5..c59202a0a45 100644 --- a/query/src/org/labkey/query/reports/ReportsController.java +++ b/query/src/org/labkey/query/reports/ReportsController.java @@ -174,7 +174,6 @@ import javax.script.ScriptException; import java.io.File; import java.io.IOException; -import java.io.PrintWriter; import java.io.StringWriter; import java.net.URISyntaxException; import java.net.URL; @@ -307,12 +306,6 @@ public ActionURL urlImage(Container c, Report r, ImageType type, @Nullable Integ return url; } - @Override - public ActionURL urlReportInfo(Container c) - { - return new ActionURL(ReportInfoAction.class, c); - } - @Override public ActionURL urlAttachmentReport(Container c, ActionURL returnUrl) { @@ -326,9 +319,12 @@ public ActionURL urlLinkReport(Container c, ActionURL returnUrl) } @Override - public ActionURL urlReportDetails(Container c, Report r) + public ActionURL urlReportDetails(Container c, @Nullable Report report) { - return new ActionURL(DetailsAction.class, c).addParameter(ReportDescriptor.Prop.reportId, r.getDescriptor().getReportId().toString()); + ActionURL url = new ActionURL(DetailsAction.class, c); + if (report != null) + url.addParameter(ReportDescriptor.Prop.reportId, report.getDescriptor().getReportId().toString()); + return url; } @Override @@ -1173,66 +1169,6 @@ public void addNavTrail(NavTree root) } } - @RequiresPermission(ReadPermission.class) - public static class ReportInfoAction extends SimpleViewAction> - { - @Override - public ModelAndView getView(ReportDesignBean form, BindException errors) throws Exception - { - return new ReportInfoView(form.getReport(getViewContext())); - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Report Debug Information"); - } - } - - - public static class ReportInfoView extends HttpView - { - private final Report _report; - - public ReportInfoView(Report report) - { - _report = report; - } - - @Override - protected void renderInternal(Object model, PrintWriter out) - { - if (_report != null) - { - out.write(""); - addRow(out, "Name", PageFlowUtil.filter(_report.getDescriptor().getReportName())); - - User user = UserManager.getUser(_report.getDescriptor().getCreatedBy()); - if (user != null) - addRow(out, "Created By", PageFlowUtil.filter(user.getDisplayName(getViewContext().getUser()))); - - addRow(out, "Key", PageFlowUtil.filter(_report.getDescriptor().getReportKey())); - for (Map.Entry prop : _report.getDescriptor().getProperties().entrySet()) - { - addRow(out, PageFlowUtil.filter(prop.getKey()), PageFlowUtil.filter(Objects.toString(prop.getValue(), ""))); - } - out.write("
"); - } - else - out.write("Report not found"); - } - - private void addRow(PrintWriter out, String key, String value) - { - out.write(""); - } - } - - protected void validatePermissions(ViewContext context, ScriptReport report, List errors) { if (report != null) @@ -1718,7 +1654,7 @@ public boolean saveReport(F form, BindException errors) throws Exception { // Convey previous state to save code, otherwise admins will be denied the ability to unshare. descriptor.setWasShared(); - descriptor.setOwner(descriptor.getCreatedBy()); + descriptor.setOwner(descriptor.getCreatedBy() != 0 ? descriptor.getCreatedBy() : getUser().getUserId()); } } else diff --git a/query/src/org/labkey/query/view/internalDeleteView.jsp b/query/src/org/labkey/query/view/internalDeleteView.jsp deleted file mode 100644 index 90bab721e7b..00000000000 --- a/query/src/org/labkey/query/view/internalDeleteView.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<% -/* - * Copyright (c) 2013-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -%> -<%@ page import="org.labkey.api.view.HttpView" %> -<%@ page import="org.labkey.query.controllers.InternalViewForm" %> -<%@ page import="org.labkey.query.persist.CstmView" %> -<%@ page extends="org.labkey.api.jsp.JspBase" %> -<% - InternalViewForm form = (InternalViewForm)HttpView.currentModel(); - CstmView view = form.getViewAndCheckPermission(); -%> -

Are you sure you want to delete this view?

-

Schema: <%=h(view.getSchema())%>
- Query: <%=h(view.getQueryName())%>
- View Name: <%=h(view.getName())%>
- Owner: <%=h(String.valueOf(view.getCustomViewOwner()))%> -

\ No newline at end of file diff --git a/query/src/org/labkey/query/view/internalNewView.jsp b/query/src/org/labkey/query/view/internalNewView.jsp deleted file mode 100644 index 6a0f8eb9ef1..00000000000 --- a/query/src/org/labkey/query/view/internalNewView.jsp +++ /dev/null @@ -1,39 +0,0 @@ -<% -/* - * Copyright (c) 2013-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -%> -<%@ page import="org.labkey.api.view.ActionURL" %> -<%@ page import="org.labkey.api.view.HttpView" %> -<%@ page import="org.labkey.query.controllers.InternalNewViewForm" %> -<%@ page import="org.labkey.query.controllers.QueryController.InternalNewViewAction" %> -<%@ page import="org.labkey.query.controllers.QueryController.ManageViewsAction" %> -<%@ page extends="org.labkey.api.jsp.JspBase"%> -<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> -<% - InternalNewViewForm form = (InternalNewViewForm) HttpView.currentModel(); - ActionURL urlPost = new ActionURL(InternalNewViewAction.class, getContainer()); - ActionURL urlCancel = new ActionURL(ManageViewsAction.class, getContainer()); -%> - - -

Create New Custom View

-

Schema Name:

-

Query Name:

-

View Name:

-

> Share with other users

-

> Inherit view in sub-projects

- -
diff --git a/query/src/org/labkey/query/view/internalSourceView.jsp b/query/src/org/labkey/query/view/internalSourceView.jsp deleted file mode 100644 index 05ad7648d11..00000000000 --- a/query/src/org/labkey/query/view/internalSourceView.jsp +++ /dev/null @@ -1,75 +0,0 @@ -<% -/* - * Copyright (c) 2013-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -%> -<%@ page import="org.labkey.api.security.User" %> -<%@ page import="org.labkey.api.security.UserManager" %> -<%@ page import="org.labkey.api.view.ActionURL" %> -<%@ page import="org.labkey.api.view.HttpView" %> -<%@ page import="org.labkey.query.controllers.InternalSourceViewForm" %> -<%@ page import="org.labkey.query.controllers.QueryController.InternalSourceViewAction" %> -<%@ page import="org.labkey.query.controllers.QueryController.ManageViewsAction" %> -<%@ page import="org.labkey.query.persist.CstmView" %> -<%@ page extends="org.labkey.api.jsp.JspBase" %> -<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> -<%! - String userIdToString(Integer userId) - { - if (userId == null) - { - return ""; - } - User user = UserManager.getUser(userId); - if (user == null) - { - return "Unknown user #" + userId; - } - return user.getDisplayName(user); - } -%> -<% - InternalSourceViewForm form = (InternalSourceViewForm) HttpView.currentModel(); - ActionURL urlPost = new ActionURL(InternalSourceViewAction.class, getContainer()); - urlPost.addParameter("customViewId", Integer.toString(form.getCustomViewId())); - ActionURL urlCancel = new ActionURL(ManageViewsAction.class, getContainer()); - CstmView view = form.getViewAndCheckPermission(); -%> - - -

- Schema: <%=h(view.getSchema())%>
- Query: <%=h(view.getQueryName())%>
- Name: <%=h(view.getName())%>
- Owner: <%=h(userIdToString(view.getCustomViewOwner()))%>
-
- Inherit:
- Hidden:
- Shared: <%=h(view.getCustomViewOwner() == null ? "yes" : "no")%>
-
- Container: <%=h(view.getContainerPath())%>
- Created: <%=formatDateTime(view.getCreated())%>
- Created By: <%=h(userIdToString(view.getCreatedBy()))%>
- Modified: <%=formatDateTime(view.getModified())%>
- Modified: <%=h(userIdToString(view.getModifiedBy()))%>
-

-
"); - out.write(key); - out.write(""); - out.write(value); - out.write("
- - - -
ColumnsFilter/Sort
- - - diff --git a/query/src/org/labkey/query/view/manageViews.jsp b/query/src/org/labkey/query/view/manageViews.jsp deleted file mode 100644 index d4f52746f04..00000000000 --- a/query/src/org/labkey/query/view/manageViews.jsp +++ /dev/null @@ -1,169 +0,0 @@ -<% -/* - * Copyright (c) 2013-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -%> -<%@ page import="org.apache.commons.lang3.StringUtils" %> -<%@ page import="org.labkey.api.data.Container" %> -<%@ page import="org.labkey.api.query.QueryForm" %> -<%@ page import="org.labkey.api.security.User" %> -<%@ page import="org.labkey.api.security.UserManager" %> -<%@ page import="org.labkey.api.security.permissions.UpdatePermission" %> -<%@ page import="org.labkey.api.view.ActionURL" %> -<%@ page import="org.labkey.api.view.HttpView" %> -<%@ page import="org.labkey.query.controllers.QueryController.InternalDeleteView" %> -<%@ page import="org.labkey.query.controllers.QueryController.InternalNewViewAction" %> -<%@ page import="org.labkey.query.controllers.QueryController.InternalSourceViewAction" %> -<%@ page import="org.labkey.query.persist.CstmView" %> -<%@ page import="org.labkey.query.persist.QueryManager" %> -<%@ page import="org.labkey.query.view.CustomViewSetKey" %> -<%@ page import="java.util.ArrayList" %> -<%@ page import="java.util.List" %> -<%@ page import="java.util.Objects" %> -<%@ page import="org.labkey.api.query.SchemaKey" %> -<%@ page extends="org.labkey.api.jsp.JspBase" %> -<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> -<%! - String userIdToString(Integer userId, User currentUser) - { - if (userId == null) - { - return ""; - } - User user = UserManager.getUser(userId); - if (user == null) - return "Unknown user #" + userId; - if (user.isGuest()) - return "Guest"; - return user.getDisplayName(currentUser); - } -%> -<% - QueryForm form = (QueryForm) HttpView.currentModel(); - User user = getUser(); - Container c = getContainer(); - String schemaName = form.getSchemaName().isEmpty() ? null : form.getSchemaName(); - String queryName = form.getQueryName(); - QueryManager mgr = QueryManager.get(); - List views = new ArrayList<>(); - - if (getViewContext().getUser().hasSiteAdminPermission()) - { - views.addAll(mgr.getCstmViews(c, schemaName, queryName, null, null, false, false)); - } - else - { - if (getViewContext().hasPermission(UpdatePermission.class)) - views.addAll(mgr.getCstmViews(c, schemaName, queryName, null, null, false, true)); - if (!user.isGuest()) - views.addAll(mgr.getCstmViews(c, schemaName, queryName, null, user, false, false)); - } - - // UNDONE: Requires queryName and schemaName for now. We need a method to get all session views in a container. - if (queryName != null && schemaName != null && !schemaName.isEmpty()) - { - views.addAll(CustomViewSetKey.getCustomViewsFromSession(getViewContext().getRequest(), c, queryName, SchemaKey.fromString(schemaName)).values()); - } - - views.sort((o1, o2) -> - { - if (o1 == o2) - return 0; - Integer owner1 = o1.getCustomViewOwner(); - Integer owner2 = o2.getCustomViewOwner(); - if (!Objects.equals(owner1, owner2)) - { - if (owner1 == null) - return -1; - if (owner2 == null) - return 1; - return owner1 - owner2; - } - int ret = StringUtils.trimToEmpty(o1.getSchema()).compareToIgnoreCase(StringUtils.trimToEmpty(o2.getSchema())); - if (ret != 0) - return ret; - ret = StringUtils.trimToEmpty(o1.getQueryName()).compareToIgnoreCase(StringUtils.trimToEmpty(o2.getQueryName())); - if (ret != 0) - return ret; - return StringUtils.trimToEmpty(o1.getName()).compareToIgnoreCase(StringUtils.trimToEmpty(o2.getName())); - }); -%> -

This page is for troubleshooting custom grid views. It is not intended for general use. -<% if (schemaName != null) { %> -
Filtered by schema: <%= h(schemaName) %> -<% } %> -<% if (queryName != null) { %> -
Filtered by query: <%= h(queryName) %> -<% } %> -

- - - - - - - - - - - - - - - <% if (getViewContext().hasPermission(UpdatePermission.class)) - { - int count = 1; - for (CstmView view : views) - { - count++; - List flags = new ArrayList<>(); - if (view.getCustomViewId() == 0) - flags.add("session"); - if (mgr.canInherit(view.getFlags())) - flags.add("inherit"); - if (mgr.isHidden(view.getFlags())) - flags.add("hidden"); - if (mgr.isSnapshot(view.getFlags())) - flags.add("shapshot"); - %> - - - - - - - - - - - - - <% - } - }%> -
SchemaQueryView NameFlagsOwnerCreatedCreated ByModifiedModified By
<%=h(view.getSchema())%> - <%=h(view.getQueryName())%> - <%=h(view.getName())%> - <%=unsafe(StringUtils.join(flags, ","))%><%=h(view.isShared() ? "" : userIdToString(view.getCustomViewOwner(), user))%> - <%=formatDateTime(view.getCreated())%><%=h(userIdToString(view.getCreatedBy(), user))%><%=formatDateTime(view.getModified())%><%=h(userIdToString(view.getModifiedBy(), user))%><% ActionURL urlDelete = new ActionURL(InternalDeleteView.class, c); - urlDelete.addParameter("customViewId", Integer.toString(view.getCustomViewId())); %> - <%=link("delete", urlDelete)%> - <% ActionURL urlSource = new ActionURL(InternalSourceViewAction.class, c); - urlSource.addParameter("customViewId", Integer.toString(view.getCustomViewId())); %> - <%=link("edit", urlSource)%> -
- -<% ActionURL urlNewView = new ActionURL(InternalNewViewAction.class, c); %> -