Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api/src/org/labkey/api/reports/report/AbstractReport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions api/src/org/labkey/api/reports/report/ReportDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions api/src/org/labkey/api/reports/report/ReportUrls.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion core/src/org/labkey/core/CoreModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
6 changes: 6 additions & 0 deletions core/src/org/labkey/core/query/CoreQuerySchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -145,6 +146,9 @@ public Set<String> 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);

Expand Down Expand Up @@ -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;
}
Expand Down
249 changes: 249 additions & 0 deletions core/src/org/labkey/core/query/ReportsTable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/*
* Copyright (c) 2008-2026 LabKey Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.labkey.core.query;

import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.labkey.api.collections.CaseInsensitiveHashMap;
import org.labkey.api.data.ColumnInfo;
import org.labkey.api.data.Container;
import org.labkey.api.data.ContainerFilter;
import org.labkey.api.data.CoreSchema;
import org.labkey.api.data.JdbcType;
import org.labkey.api.data.SQLFragment;
import org.labkey.api.data.TableInfo;
import org.labkey.api.query.BatchValidationException;
import org.labkey.api.query.DefaultQueryUpdateService;
import org.labkey.api.query.DetailsURL;
import org.labkey.api.query.ExprColumn;
import org.labkey.api.query.FieldKey;
import org.labkey.api.query.FilteredTable;
import org.labkey.api.query.QueryService;
import org.labkey.api.query.QueryUpdateService;
import org.labkey.api.query.column.BuiltInColumnTypes;
import org.labkey.api.reports.ReportService;
import org.labkey.api.reports.report.QueryReport;
import org.labkey.api.reports.report.ReportDescriptor;
import org.labkey.api.reports.report.ReportUrls;
import org.labkey.api.security.User;
import org.labkey.api.security.UserPrincipal;
import org.labkey.api.security.permissions.AdminPermission;
import org.labkey.api.security.permissions.DeletePermission;
import org.labkey.api.security.permissions.Permission;
import org.labkey.api.security.permissions.ReadPermission;
import org.labkey.api.util.ContainerContext;
import org.labkey.api.util.JunitUtil;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.util.TestContext;
import org.labkey.api.util.logging.LogHelper;
import org.labkey.api.view.ActionURL;
import org.labkey.api.view.UnauthorizedException;
import org.labkey.api.writer.ContainerUser;

import java.util.List;
import java.util.Map;

import static org.labkey.api.util.JunitUtil.deleteTestContainer;

public class ReportsTable extends FilteredTable<CoreQuerySchema>
{
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<? extends Permission> 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<String, Object> deleteRow(User user, Container container, Map<String, Object> 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;
}
}

public static class TestCase extends Assert
{
private static final Logger LOG = LogHelper.getLogger(ReportsTable.class, "Integration tests for the ReportsTable");
private static User _user;
private static Container _container;

@BeforeClass
public static void setup() throws Exception
{
_container = JunitUtil.getTestContainer();
_user = TestContext.get().getUser();
}

@AfterClass
public static void cleanup()
{
deleteTestContainer();
_container = null;
_user = null;
}

@Test
public void testReportsTableAdminOnlyAccess()
{
LOG.info("Validate Core.Reports is admin only");

var schema = QueryService.get().getUserSchema(User.getAdminServiceUser(), _container, CoreQuerySchema.NAME);
assertNotNull("Expected admin access to the " + CoreQuerySchema.REPORTS_TABLE_NAME + " table", schema.getTable(CoreQuerySchema.REPORTS_TABLE_NAME));

schema = QueryService.get().getUserSchema(User.getSearchUser(), _container, CoreQuerySchema.NAME);
assertNull("Expected admin access to the " + CoreQuerySchema.REPORTS_TABLE_NAME + " table", schema.getTable(CoreQuerySchema.REPORTS_TABLE_NAME));
}

private QueryUpdateService ensureUpdateService(String tableName)
{
var schema = QueryService.get().getUserSchema(_user, _container, CoreQuerySchema.NAME);
var table = schema.getTable(tableName);
var qus = table.getUpdateService();
assertNotNull("Expected update service for " + tableName, qus);

return qus;
}

@Test
public void testReportsApiAccess() throws Exception
{
var qus = ensureUpdateService(CoreQuerySchema.REPORTS_TABLE_NAME);

try
{
BatchValidationException errors = new BatchValidationException();
Map<String, Object> row = CaseInsensitiveHashMap.of(
"reportKey", "foo/bar",
"hidden", true
);
qus.insertRows(_user, _container, List.of(row), errors, null, null);
fail("Insert should have thrown UnauthorizedException");
}
catch (UnauthorizedException e)
{
// expected
}

// Save a report through the service
var queryReport = ReportService.get().createReportInstance(QueryReport.TYPE);
var descriptor = queryReport.getDescriptor();
descriptor.setReportName("custom query report");

var identifier = ReportService.get().saveReportEx(ContainerUser.create(_container, _user), "reportKey", queryReport, true);
assertTrue("Unable to save a query report", identifier.getRowId() != 0);

var savedReport = identifier.getReport(ContainerUser.create(_container, _user));
assertNotNull("Unable to retrieve a saved report", savedReport);

BatchValidationException errors = new BatchValidationException();
Map<String, Object> row = CaseInsensitiveHashMap.of(
"rowId", savedReport.getDescriptor().getReportId().getRowId(),
"flags", 2
);

try
{
qus.updateRows(_user, _container, List.of(row), null, errors, null, null);
fail("Update should have thrown UnauthorizedException");
}
catch (UnauthorizedException e)
{
// expected, delete the query
qus.deleteRows(_user, _container, List.of(row), null, null);
}
}
}
}
4 changes: 3 additions & 1 deletion query/src/org/labkey/query/QueryModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ public void doStartup(ModuleContext moduleContext)
);

McpService.get().register(new QueryMcp());
QueryUserSchema.register(this);
}

@Override
Expand Down Expand Up @@ -432,7 +433,8 @@ public Set<String> getSchemaNames()
Query.TestCase.class,
ReportsController.SerializationTest.class,
SqlParser.SqlParserTestCase.class,
TableWriter.TestCase.class
TableWriter.TestCase.class,
QueryUserSchema.TestCase.class
);
}

Expand Down
Loading
Loading