From 78132b3c27ae065b045c1991b75ce04982063eaf Mon Sep 17 00:00:00 2001 From: Lum Date: Wed, 29 Apr 2026 13:09:46 -0700 Subject: [PATCH 01/12] Create and expose a custom views table in the new query schema --- query/src/org/labkey/query/QueryModule.java | 1 + .../src/org/labkey/query/QueryUserSchema.java | 61 ++++++++ .../controllers/InternalNewViewForm.java | 4 +- .../query/controllers/QueryController.java | 3 +- .../labkey/query/query/CustomViewsTable.java | 140 ++++++++++++++++++ .../org/labkey/query/query/QueryDbSchema.java | 35 +++++ .../org/labkey/query/view/internalNewView.jsp | 7 +- .../labkey/query/view/internalSourceView.jsp | 8 +- 8 files changed, 252 insertions(+), 7 deletions(-) create mode 100644 query/src/org/labkey/query/QueryUserSchema.java create mode 100644 query/src/org/labkey/query/query/CustomViewsTable.java create mode 100644 query/src/org/labkey/query/query/QueryDbSchema.java diff --git a/query/src/org/labkey/query/QueryModule.java b/query/src/org/labkey/query/QueryModule.java index 0e57baef4de..bd84434c32c 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 diff --git a/query/src/org/labkey/query/QueryUserSchema.java b/query/src/org/labkey/query/QueryUserSchema.java new file mode 100644 index 00000000000..dd633f01b8b --- /dev/null +++ b/query/src/org/labkey/query/QueryUserSchema.java @@ -0,0 +1,61 @@ +package org.labkey.query; + +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.DefaultSchema; +import org.labkey.api.query.QuerySchema; +import org.labkey.api.query.UserSchema; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.query.query.CustomViewsTable; +import org.labkey.query.query.QueryDbSchema; + +import java.util.HashSet; +import java.util.Set; + +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"; + + 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()); + } + }); + } + + 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); + + return names; + } + + @Override + public TableInfo createTable(String name, ContainerFilter cf) + { + if (CUSTOM_VIEWS_TABLE_NAME.equalsIgnoreCase(name) && getContainer().hasPermission(getUser(), AdminPermission.class)) + return new CustomViewsTable(this, cf); + + return null; + } +} diff --git a/query/src/org/labkey/query/controllers/InternalNewViewForm.java b/query/src/org/labkey/query/controllers/InternalNewViewForm.java index 7fcb8089c6e..cfcafe022e0 100644 --- a/query/src/org/labkey/query/controllers/InternalNewViewForm.java +++ b/query/src/org/labkey/query/controllers/InternalNewViewForm.java @@ -16,7 +16,9 @@ package org.labkey.query.controllers; -public class InternalNewViewForm +import org.labkey.api.action.ReturnUrlForm; + +public class InternalNewViewForm extends ReturnUrlForm { public String ff_schemaName; public String ff_queryName; diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 14b4cfa9bce..aaf207e8277 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -6422,7 +6422,7 @@ public boolean handlePost(InternalSourceViewForm form, BindException errors) @Override public ActionURL getSuccessURL(InternalSourceViewForm form) { - return new ActionURL(ManageViewsAction.class, getContainer()); + return form.getReturnActionURL(); } @Override @@ -6511,6 +6511,7 @@ public ActionURL getSuccessURL(InternalNewViewForm form) { ActionURL forward = new ActionURL(InternalSourceViewAction.class, getContainer()); forward.addParameter("customViewId", Integer.toString(_customViewId)); + forward.addReturnUrl(form.getReturnActionURL()); return forward; } 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..ebb3f36f22c --- /dev/null +++ b/query/src/org/labkey/query/query/CustomViewsTable.java @@ -0,0 +1,140 @@ +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.TableInfo; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DefaultQueryUpdateService; +import org.labkey.api.query.DetailsURL; +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.EditSharedViewPermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.UnauthorizedException; +import org.labkey.query.QueryUserSchema; +import org.labkey.query.controllers.QueryController; +import org.labkey.query.persist.QueryManager; + +import java.sql.SQLException; +import java.util.Collections; +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 to folder administrators."); + + setImportURL(LINK_DISABLER); + setUpdateURL(new DetailsURL(new ActionURL(QueryController.InternalSourceViewAction.class, getContainer()), Collections.singletonMap("CustomViewId", "CustomViewId"))); + setInsertURL(new DetailsURL(new ActionURL(QueryController.InternalNewViewAction.class, getContainer()))); + + 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"); + + setDefaultVisibleColumns(List.of( + FieldKey.fromParts("Schema"), + FieldKey.fromParts("QueryName"), + FieldKey.fromParts("Name"), + FieldKey.fromParts("CustomViewOwner"), + FieldKey.fromParts("Container"), + FieldKey.fromParts("Flags"), + 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()); + } + + protected static class CustomViewsUpdateService extends DefaultQueryUpdateService + { + public CustomViewsUpdateService(TableInfo queryTable, TableInfo dbTable) + { + super(queryTable, dbTable); + } + + @Override + protected Map deleteRow(User user, Container container, Map oldRowMap) throws QueryUpdateServiceException, SQLException, InvalidKeyException + { + Integer id = (Integer)oldRowMap.get("customViewId"); + if (id != null) + { + var view = QueryManager.get().getCustomView(container, id); + if (view != null) + { + if (view.getCustomViewOwner() == null) + { + if (!container.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 oldRowMap; + } + + @Override + public List> insertRows(User user, Container container, List> rows, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) + { + // for now just rely on the existing internal action + throw new UnsupportedOperationException(); + } + + @Override + public List> updateRows(User user, Container container, List> rows, List> oldKeys, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException + { + // for now just rely on the existing internal action + throw new UnsupportedOperationException(); + } + } +} 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..9cc9c3e1ee2 --- /dev/null +++ b/query/src/org/labkey/query/query/QueryDbSchema.java @@ -0,0 +1,35 @@ +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"); + } +} diff --git a/query/src/org/labkey/query/view/internalNewView.jsp b/query/src/org/labkey/query/view/internalNewView.jsp index 6a0f8eb9ef1..11da1777014 100644 --- a/query/src/org/labkey/query/view/internalNewView.jsp +++ b/query/src/org/labkey/query/view/internalNewView.jsp @@ -19,16 +19,19 @@ <%@ 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()); + if (form.getReturnActionURL() != null) + urlPost.addReturnUrl(form.getReturnActionURL()); + + ActionURL urlCancel = form.getReturnActionURL(); %> +

Create New Custom View

Schema Name:

Query Name:

diff --git a/query/src/org/labkey/query/view/internalSourceView.jsp b/query/src/org/labkey/query/view/internalSourceView.jsp index 05ad7648d11..10149ff2057 100644 --- a/query/src/org/labkey/query/view/internalSourceView.jsp +++ b/query/src/org/labkey/query/view/internalSourceView.jsp @@ -21,7 +21,6 @@ <%@ 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" %> @@ -43,12 +42,15 @@ <% 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()); + if (form.getReturnActionURL() != null) + urlPost.addReturnUrl(form.getReturnActionURL()); + + ActionURL urlCancel = form.getReturnActionURL(); CstmView view = form.getViewAndCheckPermission(); %> +

Schema: <%=h(view.getSchema())%>
Query: <%=h(view.getQueryName())%>
From e4928dd1c6b3dcd5a3e0c7a2eaef254b77d6d030 Mon Sep 17 00:00:00 2001 From: Lum Date: Wed, 29 Apr 2026 15:02:17 -0700 Subject: [PATCH 02/12] Display column for flag field --- .../labkey/query/query/CustomViewsTable.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/query/src/org/labkey/query/query/CustomViewsTable.java b/query/src/org/labkey/query/query/CustomViewsTable.java index ebb3f36f22c..a39a8b78be0 100644 --- a/query/src/org/labkey/query/query/CustomViewsTable.java +++ b/query/src/org/labkey/query/query/CustomViewsTable.java @@ -2,8 +2,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.DataColumn; +import org.labkey.api.data.RenderContext; import org.labkey.api.data.TableInfo; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.DefaultQueryUpdateService; @@ -19,6 +22,7 @@ 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.ActionURL; import org.labkey.api.view.UnauthorizedException; import org.labkey.query.QueryUserSchema; @@ -26,6 +30,7 @@ import org.labkey.query.persist.QueryManager; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,6 +68,7 @@ public CustomViewsTable(@NotNull QueryUserSchema userSchema, ContainerFilter cf) getMutableColumnOrThrow("ModifiedBy").setConceptURI(BuiltInColumnTypes.USERID_CONCEPT_URI); getMutableColumnOrThrow("Columns"); getMutableColumnOrThrow("Filter"); + getMutableColumnOrThrow("Flags").setDisplayColumnFactory(FlagDisplayColumn::new); setDefaultVisibleColumns(List.of( FieldKey.fromParts("Schema"), @@ -90,6 +96,32 @@ 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) From f0814170885238a3abd15d3692da8e6b407f537e Mon Sep 17 00:00:00 2001 From: Lum Date: Wed, 29 Apr 2026 15:17:36 -0700 Subject: [PATCH 03/12] Remove old manageViews action, including the delete action which is now handled in the update service for the CustomViewsTable. --- .../query/controllers/QueryController.java | 64 ------- .../labkey/query/view/internalDeleteView.jsp | 31 ---- .../src/org/labkey/query/view/manageViews.jsp | 169 ------------------ 3 files changed, 264 deletions(-) delete mode 100644 query/src/org/labkey/query/view/internalDeleteView.jsp delete mode 100644 query/src/org/labkey/query/view/manageViews.jsp diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index aaf207e8277..c882eb62a01 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; @@ -6325,66 +6324,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 @@ -6428,7 +6367,6 @@ public ActionURL getSuccessURL(InternalSourceViewForm form) @Override public void addNavTrail(NavTree root) { - new ManageViewsAction(getViewContext()).addNavTrail(root); root.addChild("Edit source of Grid View"); } } @@ -8600,8 +8538,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/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/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); %> - From a1af478aa8467c427b30c90ef4db3916d8415477 Mon Sep 17 00:00:00 2001 From: Lum Date: Wed, 29 Apr 2026 16:39:29 -0700 Subject: [PATCH 04/12] Unit test for CustomViewsTable --- query/src/org/labkey/query/QueryModule.java | 3 +- .../src/org/labkey/query/QueryUserSchema.java | 88 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/query/src/org/labkey/query/QueryModule.java b/query/src/org/labkey/query/QueryModule.java index bd84434c32c..7195986c513 100644 --- a/query/src/org/labkey/query/QueryModule.java +++ b/query/src/org/labkey/query/QueryModule.java @@ -433,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 index dd633f01b8b..4a6fc3bc0b1 100644 --- a/query/src/org/labkey/query/QueryUserSchema.java +++ b/query/src/org/labkey/query/QueryUserSchema.java @@ -1,20 +1,35 @@ 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.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.query.query.CustomViewsTable; 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"; @@ -58,4 +73,77 @@ public TableInfo createTable(String name, ContainerFilter 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() throws Exception + { + 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 " + CUSTOM_VIEWS_TABLE_NAME + " table", schema.getTable(CUSTOM_VIEWS_TABLE_NAME)); + } + + @Test + public void testCustomViewsApiAccess() throws Exception + { + var schema = QueryService.get().getUserSchema(_user, _container, SCHEMA_NAME); + var table = schema.getTable(CUSTOM_VIEWS_TABLE_NAME); + var qus = table.getUpdateService(); + assertNotNull("Expected update service for " + CUSTOM_VIEWS_TABLE_NAME, qus); + + BatchValidationException errors = new BatchValidationException(); + try + { + Map row = CaseInsensitiveHashMap.of( + "schemaName", "test", + "queryName", "query" + ); + qus.insertRows(_user, _container, List.of(row), errors, null, null); + assertTrue("Expected insert to error", errors.hasErrors()); + } + catch (UnsupportedOperationException e) + { + // expected + } + + try + { + Map row = CaseInsensitiveHashMap.of( + "customViewId", 1, + "schemaName", "test", + "queryName", "query" + ); + qus.updateRows(_user, _container, List.of(row), null, errors, null, null); + assertTrue("Expected update to error", errors.hasErrors()); + } + catch (UnsupportedOperationException e) + { + // expected + } + } + } } From 6dc5bc2986949b8c7f01219934ce761b3eddb7de Mon Sep 17 00:00:00 2001 From: Lum Date: Thu, 30 Apr 2026 15:51:48 -0700 Subject: [PATCH 05/12] Selenium test for custom views query. --- query/src/org/labkey/query/QueryUserSchema.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/query/src/org/labkey/query/QueryUserSchema.java b/query/src/org/labkey/query/QueryUserSchema.java index 4a6fc3bc0b1..5ebf0907260 100644 --- a/query/src/org/labkey/query/QueryUserSchema.java +++ b/query/src/org/labkey/query/QueryUserSchema.java @@ -46,6 +46,12 @@ public QuerySchema createSchema(DefaultSchema schema, Module module) { return new QueryUserSchema(schema.getUser(), schema.getContainer()); } + + @Override + public boolean isAvailable(DefaultSchema schema, Module module) + { + return true; + } }); } From ea4913e233ac1d8f7a3ae3c2d78709d84f950189 Mon Sep 17 00:00:00 2001 From: lum Date: Fri, 1 May 2026 16:10:00 -0700 Subject: [PATCH 06/12] split flags field, replace legacy forms, use QUS for CRUD --- .../src/org/labkey/query/QueryUserSchema.java | 40 ++----- .../org/labkey/query/persist/CstmView.java | 23 ++++ .../labkey/query/query/CustomViewsTable.java | 109 ++++++++++++++++-- 3 files changed, 132 insertions(+), 40 deletions(-) diff --git a/query/src/org/labkey/query/QueryUserSchema.java b/query/src/org/labkey/query/QueryUserSchema.java index 5ebf0907260..457dbaab25f 100644 --- a/query/src/org/labkey/query/QueryUserSchema.java +++ b/query/src/org/labkey/query/QueryUserSchema.java @@ -122,34 +122,18 @@ public void testCustomViewsApiAccess() throws Exception assertNotNull("Expected update service for " + CUSTOM_VIEWS_TABLE_NAME, qus); BatchValidationException errors = new BatchValidationException(); - try - { - Map row = CaseInsensitiveHashMap.of( - "schemaName", "test", - "queryName", "query" - ); - qus.insertRows(_user, _container, List.of(row), errors, null, null); - assertTrue("Expected insert to error", errors.hasErrors()); - } - catch (UnsupportedOperationException e) - { - // expected - } - - try - { - Map row = CaseInsensitiveHashMap.of( - "customViewId", 1, - "schemaName", "test", - "queryName", "query" - ); - qus.updateRows(_user, _container, List.of(row), null, errors, null, null); - assertTrue("Expected update to error", errors.hasErrors()); - } - catch (UnsupportedOperationException e) - { - // expected - } + 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()); } } } 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 index a39a8b78be0..cbd98d095ad 100644 --- a/query/src/org/labkey/query/query/CustomViewsTable.java +++ b/query/src/org/labkey/query/query/CustomViewsTable.java @@ -2,20 +2,24 @@ 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.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.DetailsURL; +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; @@ -23,15 +27,14 @@ 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.ActionURL; +import org.labkey.api.view.NotFoundException; import org.labkey.api.view.UnauthorizedException; import org.labkey.query.QueryUserSchema; -import org.labkey.query.controllers.QueryController; +import org.labkey.query.persist.CstmView; import org.labkey.query.persist.QueryManager; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -45,8 +48,8 @@ public CustomViewsTable(@NotNull QueryUserSchema userSchema, ContainerFilter cf) setDescription("Contains a row for each saved custom view. Available to folder administrators."); setImportURL(LINK_DISABLER); - setUpdateURL(new DetailsURL(new ActionURL(QueryController.InternalSourceViewAction.class, getContainer()), Collections.singletonMap("CustomViewId", "CustomViewId"))); - setInsertURL(new DetailsURL(new ActionURL(QueryController.InternalNewViewAction.class, getContainer()))); + //setUpdateURL(new DetailsURL(new ActionURL(QueryController.InternalSourceViewAction.class, getContainer()), Collections.singletonMap("CustomViewId", "CustomViewId"))); + //setInsertURL(new DetailsURL(new ActionURL(QueryController.InternalNewViewAction.class, getContainer()))); wrapAllColumns(true); var customViewIdCol = getMutableColumnOrThrow("CustomViewId"); @@ -70,13 +73,29 @@ public CustomViewsTable(@NotNull QueryUserSchema userSchema, ContainerFilter cf) 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("Flags"), + FieldKey.fromParts("Hidden"), + FieldKey.fromParts("Inheritable"), FieldKey.fromParts("Created"), FieldKey.fromParts("CreatedBy"), FieldKey.fromParts("Modified"), @@ -158,15 +177,81 @@ protected Map deleteRow(User user, Container container, Map> insertRows(User user, Container container, List> rows, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) { - // for now just rely on the existing internal action - throw new UnsupportedOperationException(); + 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 - public List> updateRows(User user, Container container, List> rows, List> oldKeys, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException + protected Map _update(User user, Container container, Map row, Map oldRow, Object[] keys) throws SQLException, ValidationException { - // for now just rely on the existing internal action - throw new UnsupportedOperationException(); + Integer id = (Integer) oldRow.get("customViewId"); + CstmView view = QueryManager.get().getCustomView(container, 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, container, user); + return CstmView.toRow(QueryManager.get().update(user, view)); + } + + 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(); + } + } } } } From a45b6c7b2c3f8277d19c0a2d1e7b3ad1f0b02e78 Mon Sep 17 00:00:00 2001 From: Lum Date: Mon, 4 May 2026 11:47:14 -0700 Subject: [PATCH 07/12] Delete unused actions. --- .../controllers/InternalNewViewForm.java | 53 ------- .../controllers/InternalSourceViewForm.java | 55 ------- .../query/controllers/InternalViewForm.java | 79 ---------- .../query/controllers/QueryController.java | 140 ------------------ .../labkey/query/query/CustomViewsTable.java | 3 - .../org/labkey/query/view/internalNewView.jsp | 42 ------ .../labkey/query/view/internalSourceView.jsp | 77 ---------- 7 files changed, 449 deletions(-) delete mode 100644 query/src/org/labkey/query/controllers/InternalNewViewForm.java delete mode 100644 query/src/org/labkey/query/controllers/InternalSourceViewForm.java delete mode 100644 query/src/org/labkey/query/controllers/InternalViewForm.java delete mode 100644 query/src/org/labkey/query/view/internalNewView.jsp delete mode 100644 query/src/org/labkey/query/view/internalSourceView.jsp 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 cfcafe022e0..00000000000 --- a/query/src/org/labkey/query/controllers/InternalNewViewForm.java +++ /dev/null @@ -1,53 +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.action.ReturnUrlForm; - -public class InternalNewViewForm extends ReturnUrlForm -{ - 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 c882eb62a01..427e550af11 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -292,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; @@ -6324,143 +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 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 form.getReturnActionURL(); - } - - @Override - public void addNavTrail(NavTree 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)); - forward.addReturnUrl(form.getReturnActionURL()); - return forward; - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Create New Grid View"); - } - } - - @ActionNames("clearSelected, selectNone") @RequiresPermission(ReadPermission.class) @Action(ActionType.SelectData.class) @@ -8538,8 +8400,6 @@ controller.new NewQueryAction(), new ManageRemoteConnectionsAction(), new ReloadExternalSchemaAction(), new ReloadAllUserSchemas(), - controller.new InternalSourceViewAction(), - controller.new InternalNewViewAction(), new QueryExportAuditRedirectAction() ); diff --git a/query/src/org/labkey/query/query/CustomViewsTable.java b/query/src/org/labkey/query/query/CustomViewsTable.java index cbd98d095ad..371849db698 100644 --- a/query/src/org/labkey/query/query/CustomViewsTable.java +++ b/query/src/org/labkey/query/query/CustomViewsTable.java @@ -48,9 +48,6 @@ public CustomViewsTable(@NotNull QueryUserSchema userSchema, ContainerFilter cf) setDescription("Contains a row for each saved custom view. Available to folder administrators."); setImportURL(LINK_DISABLER); - //setUpdateURL(new DetailsURL(new ActionURL(QueryController.InternalSourceViewAction.class, getContainer()), Collections.singletonMap("CustomViewId", "CustomViewId"))); - //setInsertURL(new DetailsURL(new ActionURL(QueryController.InternalNewViewAction.class, getContainer()))); - wrapAllColumns(true); var customViewIdCol = getMutableColumnOrThrow("CustomViewId"); customViewIdCol.setKeyField(true); 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 11da1777014..00000000000 --- a/query/src/org/labkey/query/view/internalNewView.jsp +++ /dev/null @@ -1,42 +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 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()); - if (form.getReturnActionURL() != null) - urlPost.addReturnUrl(form.getReturnActionURL()); - - ActionURL urlCancel = form.getReturnActionURL(); -%> - - - -

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 10149ff2057..00000000000 --- a/query/src/org/labkey/query/view/internalSourceView.jsp +++ /dev/null @@ -1,77 +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.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()); - if (form.getReturnActionURL() != null) - urlPost.addReturnUrl(form.getReturnActionURL()); - - ActionURL urlCancel = form.getReturnActionURL(); - 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()))%>
-

- - - - -
ColumnsFilter/Sort
- - -
From be3547e3d1b11611588746e9bb006177a9a32eec Mon Sep 17 00:00:00 2001 From: Lum Date: Tue, 5 May 2026 15:41:49 -0700 Subject: [PATCH 08/12] Reports and Queries Tables. --- .../api/reports/report/AbstractReport.java | 4 +- .../api/reports/report/ReportDescriptor.java | 3 +- .../labkey/api/reports/report/ReportUrls.java | 3 +- .../labkey/core/query/CoreQuerySchema.java | 6 + .../org/labkey/core/query/ReportsTable.java | 126 ++++++++++++++++++ .../src/org/labkey/query/QueryUserSchema.java | 18 ++- .../labkey/query/query/CustomViewsTable.java | 2 +- .../org/labkey/query/query/QueriesTable.java | 114 ++++++++++++++++ .../org/labkey/query/query/QueryDbSchema.java | 5 + .../query/reports/ReportsController.java | 76 +---------- 10 files changed, 277 insertions(+), 80 deletions(-) create mode 100644 core/src/org/labkey/core/query/ReportsTable.java create mode 100644 query/src/org/labkey/query/query/QueriesTable.java 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/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..adff9cd6d2a --- /dev/null +++ b/core/src/org/labkey/core/query/ReportsTable.java @@ -0,0 +1,126 @@ +package org.labkey.core.query; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +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.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.QueryUpdateService; +import org.labkey.api.query.column.BuiltInColumnTypes; +import org.labkey.api.reports.ReportService; +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.PageFlowUtil; +import org.labkey.api.view.ActionURL; +import org.labkey.api.writer.ContainerUser; + +import java.util.List; +import java.util.Map; + +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"); + if (id != null) + { + var r = ReportService.get().getReport(container, id); + if (r != null) + ReportService.get().deleteReport(ContainerUser.create(container, user), r); + } + return oldRowMap; + } + } +} diff --git a/query/src/org/labkey/query/QueryUserSchema.java b/query/src/org/labkey/query/QueryUserSchema.java index 457dbaab25f..76aa2b5e8eb 100644 --- a/query/src/org/labkey/query/QueryUserSchema.java +++ b/query/src/org/labkey/query/QueryUserSchema.java @@ -21,6 +21,7 @@ import org.labkey.api.util.TestContext; import org.labkey.api.util.logging.LogHelper; import org.labkey.query.query.CustomViewsTable; +import org.labkey.query.query.QueriesTable; import org.labkey.query.query.QueryDbSchema; import java.util.HashSet; @@ -36,6 +37,7 @@ public class QueryUserSchema extends UserSchema 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) { @@ -50,7 +52,7 @@ public QuerySchema createSchema(DefaultSchema schema, Module module) @Override public boolean isAvailable(DefaultSchema schema, Module module) { - return true; + return schema.getContainer().hasPermission(schema.getUser(), AdminPermission.class); } }); } @@ -66,7 +68,10 @@ 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; } @@ -74,8 +79,13 @@ public Set getTableNames() @Override public TableInfo createTable(String name, ContainerFilter cf) { - if (CUSTOM_VIEWS_TABLE_NAME.equalsIgnoreCase(name) && getContainer().hasPermission(getUser(), AdminPermission.class)) - return new CustomViewsTable(this, 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; } @@ -102,7 +112,7 @@ public static void cleanup() } @Test - public void testCustomViewsAdminOnlyAccess() throws Exception + public void testCustomViewsAdminOnlyAccess() { LOG.info("Validate Query.CustomViews is admin only"); diff --git a/query/src/org/labkey/query/query/CustomViewsTable.java b/query/src/org/labkey/query/query/CustomViewsTable.java index 371849db698..84c9ceb1cc6 100644 --- a/query/src/org/labkey/query/query/CustomViewsTable.java +++ b/query/src/org/labkey/query/query/CustomViewsTable.java @@ -45,7 +45,7 @@ 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 to folder administrators."); + setDescription("Contains a row for each saved custom view. Available only to administrators."); setImportURL(LINK_DISABLER); wrapAllColumns(true); 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..20b67289626 --- /dev/null +++ b/query/src/org/labkey/query/query/QueriesTable.java @@ -0,0 +1,114 @@ +package org.labkey.query.query; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +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 + { + var queryDef = QueryDefCache.getQueryDefById(container, (Integer)oldRowMap.get("queryDefId")); + if (queryDef != null) + { + QueryDefinitionImpl queryDefImpl = new CustomQueryDefinitionImpl(user, container, queryDef); + queryDefImpl.delete(user); + } + return oldRowMap; + } + } +} diff --git a/query/src/org/labkey/query/query/QueryDbSchema.java b/query/src/org/labkey/query/query/QueryDbSchema.java index 9cc9c3e1ee2..3b0b0c0f00f 100644 --- a/query/src/org/labkey/query/query/QueryDbSchema.java +++ b/query/src/org/labkey/query/query/QueryDbSchema.java @@ -32,4 +32,9 @@ 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 From 172f326a1bbd2f4060d61be3afb5d0ea2ff8dad3 Mon Sep 17 00:00:00 2001 From: Lum Date: Wed, 6 May 2026 16:14:48 -0700 Subject: [PATCH 09/12] automation for new tables. --- core/src/org/labkey/core/CoreModule.java | 4 +- .../org/labkey/core/query/ReportsTable.java | 109 ++++++++++++++++++ .../src/org/labkey/query/QueryUserSchema.java | 87 +++++++++++++- 3 files changed, 194 insertions(+), 6 deletions(-) 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/ReportsTable.java b/core/src/org/labkey/core/query/ReportsTable.java index adff9cd6d2a..736caf4f64d 100644 --- a/core/src/org/labkey/core/query/ReportsTable.java +++ b/core/src/org/labkey/core/query/ReportsTable.java @@ -1,6 +1,12 @@ package org.labkey.core.query; +import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; +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; @@ -8,14 +14,18 @@ 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.SchemaKey; 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; @@ -25,13 +35,19 @@ 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) @@ -123,4 +139,97 @@ protected Map deleteRow(User user, Container container, Map row = CaseInsensitiveHashMap.of( + "reportKey", "foo/bar", + "hidden", true + ); + qus.insertRows(_user, _container, List.of(row), errors, null, null); + assertFalse("Insert should not be allowed", errors.hasErrors()); + } + 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); + assertFalse("Update should not be allowed", errors.hasErrors()); + } + catch (UnauthorizedException e) + { + // expected, delete the query + qus.deleteRows(_user, _container, List.of(row), null, null); + } + } + } } diff --git a/query/src/org/labkey/query/QueryUserSchema.java b/query/src/org/labkey/query/QueryUserSchema.java index 76aa2b5e8eb..f8b8ca91f5f 100644 --- a/query/src/org/labkey/query/QueryUserSchema.java +++ b/query/src/org/labkey/query/QueryUserSchema.java @@ -14,12 +14,15 @@ 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; @@ -120,16 +123,13 @@ public void testCustomViewsAdminOnlyAccess() 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 " + CUSTOM_VIEWS_TABLE_NAME + " table", schema.getTable(CUSTOM_VIEWS_TABLE_NAME)); + assertNull("Expected no reader access to the " + SCHEMA_NAME + " schema", schema); } @Test public void testCustomViewsApiAccess() throws Exception { - var schema = QueryService.get().getUserSchema(_user, _container, SCHEMA_NAME); - var table = schema.getTable(CUSTOM_VIEWS_TABLE_NAME); - var qus = table.getUpdateService(); - assertNotNull("Expected update service for " + CUSTOM_VIEWS_TABLE_NAME, qus); + var qus = ensureUpdateService(CUSTOM_VIEWS_TABLE_NAME); BatchValidationException errors = new BatchValidationException(); Map row = CaseInsensitiveHashMap.of( @@ -144,6 +144,83 @@ public void testCustomViewsApiAccess() throws Exception 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); + assertFalse("Insert should not be allowed", errors.hasErrors()); + } + 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); + assertFalse("Update should not be allowed", errors.hasErrors()); + } + 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()); } } } From 5d6c2dcf82fcd2ab7b74d1c54612f857f71703ce Mon Sep 17 00:00:00 2001 From: Lum Date: Wed, 6 May 2026 17:12:16 -0700 Subject: [PATCH 10/12] claude code feedback --- .../org/labkey/core/query/ReportsTable.java | 20 ++++++++++++++++--- .../src/org/labkey/query/QueryUserSchema.java | 19 ++++++++++++++++-- .../labkey/query/query/CustomViewsTable.java | 15 ++++++++++++++ .../org/labkey/query/query/QueriesTable.java | 15 ++++++++++++++ .../org/labkey/query/query/QueryDbSchema.java | 15 ++++++++++++++ 5 files changed, 79 insertions(+), 5 deletions(-) diff --git a/core/src/org/labkey/core/query/ReportsTable.java b/core/src/org/labkey/core/query/ReportsTable.java index 736caf4f64d..56e4141c23e 100644 --- a/core/src/org/labkey/core/query/ReportsTable.java +++ b/core/src/org/labkey/core/query/ReportsTable.java @@ -1,3 +1,18 @@ +/* + * 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; @@ -22,7 +37,6 @@ import org.labkey.api.query.FilteredTable; import org.labkey.api.query.QueryService; import org.labkey.api.query.QueryUpdateService; -import org.labkey.api.query.SchemaKey; import org.labkey.api.query.column.BuiltInColumnTypes; import org.labkey.api.reports.ReportService; import org.labkey.api.reports.report.QueryReport; @@ -196,7 +210,7 @@ public void testReportsApiAccess() throws Exception "hidden", true ); qus.insertRows(_user, _container, List.of(row), errors, null, null); - assertFalse("Insert should not be allowed", errors.hasErrors()); + fail("Insert should have thrown UnauthorizedException"); } catch (UnauthorizedException e) { @@ -223,7 +237,7 @@ public void testReportsApiAccess() throws Exception try { qus.updateRows(_user, _container, List.of(row), null, errors, null, null); - assertFalse("Update should not be allowed", errors.hasErrors()); + fail("Update should have thrown UnauthorizedException"); } catch (UnauthorizedException e) { diff --git a/query/src/org/labkey/query/QueryUserSchema.java b/query/src/org/labkey/query/QueryUserSchema.java index f8b8ca91f5f..09abd608a81 100644 --- a/query/src/org/labkey/query/QueryUserSchema.java +++ b/query/src/org/labkey/query/QueryUserSchema.java @@ -1,3 +1,18 @@ +/* + * 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; @@ -184,7 +199,7 @@ public void testQueriesApiAccess() throws Exception "sql", "SELECT * FROM test" ); qus.insertRows(_user, _container, List.of(row), errors, null, null); - assertFalse("Insert should not be allowed", errors.hasErrors()); + fail("Insert should have thrown UnauthorizedException"); } catch (UnauthorizedException e) { @@ -211,7 +226,7 @@ public void testQueriesApiAccess() throws Exception try { qus.updateRows(_user, _container, List.of(row), null, errors, null, null); - assertFalse("Update should not be allowed", errors.hasErrors()); + fail("Update should have thrown UnauthorizedException"); } catch (UnauthorizedException e) { diff --git a/query/src/org/labkey/query/query/CustomViewsTable.java b/query/src/org/labkey/query/query/CustomViewsTable.java index 84c9ceb1cc6..8fcd7728e42 100644 --- a/query/src/org/labkey/query/query/CustomViewsTable.java +++ b/query/src/org/labkey/query/query/CustomViewsTable.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/query/src/org/labkey/query/query/QueriesTable.java b/query/src/org/labkey/query/query/QueriesTable.java index 20b67289626..5742a26c185 100644 --- a/query/src/org/labkey/query/query/QueriesTable.java +++ b/query/src/org/labkey/query/query/QueriesTable.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/query/src/org/labkey/query/query/QueryDbSchema.java b/query/src/org/labkey/query/query/QueryDbSchema.java index 3b0b0c0f00f..23815336f42 100644 --- a/query/src/org/labkey/query/query/QueryDbSchema.java +++ b/query/src/org/labkey/query/query/QueryDbSchema.java @@ -1,3 +1,18 @@ +/* + * 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; From 54bd3b97131ae267d11e306c783ff948b99f1052 Mon Sep 17 00:00:00 2001 From: Lum Date: Thu, 7 May 2026 15:58:57 -0700 Subject: [PATCH 11/12] cross container delete --- core/src/org/labkey/core/query/ReportsTable.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/core/src/org/labkey/core/query/ReportsTable.java b/core/src/org/labkey/core/query/ReportsTable.java index 56e4141c23e..e4906ac796e 100644 --- a/core/src/org/labkey/core/query/ReportsTable.java +++ b/core/src/org/labkey/core/query/ReportsTable.java @@ -25,6 +25,7 @@ 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; @@ -144,11 +145,18 @@ public ReportsUpdateService(TableInfo queryTable, TableInfo dbTable) protected Map deleteRow(User user, Container container, Map oldRowMap) { Integer id = (Integer) oldRowMap.get("rowId"); - if (id != null) + String containerId = (String) oldRowMap.get("containerId"); + if (id != null && containerId != null) { - var r = ReportService.get().getReport(container, id); + var c = ContainerManager.getForId(containerId); + var r = ReportService.get().getReport(c, id); if (r != null) - ReportService.get().deleteReport(ContainerUser.create(container, user), r); + { + 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; } From ddb40b0191c9ad686128b89e9a5102c64cb5a5b5 Mon Sep 17 00:00:00 2001 From: Lum Date: Thu, 7 May 2026 16:41:16 -0700 Subject: [PATCH 12/12] cross folder issues --- .../org/labkey/core/query/ReportsTable.java | 15 ++++- .../labkey/query/query/CustomViewsTable.java | 66 ++++++++++++------- .../org/labkey/query/query/QueriesTable.java | 23 +++++-- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/core/src/org/labkey/core/query/ReportsTable.java b/core/src/org/labkey/core/query/ReportsTable.java index e4906ac796e..2a8334d5f36 100644 --- a/core/src/org/labkey/core/query/ReportsTable.java +++ b/core/src/org/labkey/core/query/ReportsTable.java @@ -17,6 +17,7 @@ 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; @@ -145,10 +146,9 @@ public ReportsUpdateService(TableInfo queryTable, TableInfo dbTable) protected Map deleteRow(User user, Container container, Map oldRowMap) { Integer id = (Integer) oldRowMap.get("rowId"); - String containerId = (String) oldRowMap.get("containerId"); - if (id != null && containerId != null) + Container c = getContainer(oldRowMap); + if (id != null && c != null) { - var c = ContainerManager.getForId(containerId); var r = ReportService.get().getReport(c, id); if (r != null) { @@ -160,6 +160,15 @@ protected Map deleteRow(User user, Container container, Map row) + { + String containerId = (String) row.get("containerId"); + if (containerId != null) + return ContainerManager.getForId(containerId); + + return null; + } } public static class TestCase extends Assert diff --git a/query/src/org/labkey/query/query/CustomViewsTable.java b/query/src/org/labkey/query/query/CustomViewsTable.java index 8fcd7728e42..0e1f7b46ddc 100644 --- a/query/src/org/labkey/query/query/CustomViewsTable.java +++ b/query/src/org/labkey/query/query/CustomViewsTable.java @@ -21,6 +21,7 @@ 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; @@ -161,17 +162,18 @@ public CustomViewsUpdateService(TableInfo queryTable, TableInfo dbTable) } @Override - protected Map deleteRow(User user, Container container, Map oldRowMap) throws QueryUpdateServiceException, SQLException, InvalidKeyException + protected Map deleteRow(User user, Container container, Map oldRow) throws QueryUpdateServiceException, SQLException, InvalidKeyException { - Integer id = (Integer)oldRowMap.get("customViewId"); - if (id != null) + Integer id = (Integer)oldRow.get("customViewId"); + Container c = getContainer(oldRow); + if (id != null && c != null) { - var view = QueryManager.get().getCustomView(container, id); + var view = QueryManager.get().getCustomView(c, id); if (view != null) { if (view.getCustomViewOwner() == null) { - if (!container.hasPermission(user, EditSharedViewPermission.class)) + if (!c.hasPermission(user, EditSharedViewPermission.class)) throw new UnauthorizedException(); } else @@ -183,7 +185,7 @@ protected Map deleteRow(User user, Container container, Map> insertRows(User user, Container container, List protected Map _update(User user, Container container, Map row, Map oldRow, Object[] keys) throws SQLException, ValidationException { Integer id = (Integer) oldRow.get("customViewId"); - CstmView view = QueryManager.get().getCustomView(container, 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); + 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); - validate(view, container, user); - return CstmView.toRow(QueryManager.get().update(user, view)); + return null; } private void validate(CstmView view, Container c, User user) diff --git a/query/src/org/labkey/query/query/QueriesTable.java b/query/src/org/labkey/query/query/QueriesTable.java index 5742a26c185..f397cb307e3 100644 --- a/query/src/org/labkey/query/query/QueriesTable.java +++ b/query/src/org/labkey/query/query/QueriesTable.java @@ -16,8 +16,10 @@ 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; @@ -117,13 +119,26 @@ public QueriesUpdateService(TableInfo queryTable, TableInfo dbTable) @Override protected Map deleteRow(User user, Container container, Map oldRowMap) throws SQLException, QueryUpdateServiceException, InvalidKeyException { - var queryDef = QueryDefCache.getQueryDefById(container, (Integer)oldRowMap.get("queryDefId")); - if (queryDef != null) + Container c = getContainer(oldRowMap); + if (c != null) { - QueryDefinitionImpl queryDefImpl = new CustomQueryDefinitionImpl(user, container, queryDef); - queryDefImpl.delete(user); + 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; + } } }
"); - out.write(key); - out.write(""); - out.write(value); - out.write("