From 226c05dbed7c6e1658d2fd260bcf8b64d668e8e1 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 6 May 2026 20:22:46 -0700 Subject: [PATCH 1/2] Molecule and PS bulk import by file --- .../src/org/labkey/audit/AuditController.java | 1170 +++++++++-------- audit/src/org/labkey/audit/AuditLogImpl.java | 631 +++++---- .../labkey/experiment/ExpDataIterators.java | 6 +- 3 files changed, 906 insertions(+), 901 deletions(-) diff --git a/audit/src/org/labkey/audit/AuditController.java b/audit/src/org/labkey/audit/AuditController.java index acd22a9e9b5..9c420cec5f8 100644 --- a/audit/src/org/labkey/audit/AuditController.java +++ b/audit/src/org/labkey/audit/AuditController.java @@ -1,584 +1,586 @@ -/* - * 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.audit; - -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.action.ApiSimpleResponse; -import org.labkey.api.action.MutatingApiAction; -import org.labkey.api.action.QueryViewAction; -import org.labkey.api.action.ReadOnlyApiAction; -import org.labkey.api.action.SimpleRedirectAction; -import org.labkey.api.action.SimpleViewAction; -import org.labkey.api.action.SpringActionController; -import org.labkey.api.admin.AdminUrls; -import org.labkey.api.audit.AbstractAuditTypeProvider; -import org.labkey.api.audit.AuditLogService; -import org.labkey.api.audit.AuditUrls; -import org.labkey.api.audit.DetailedAuditTypeEvent; -import org.labkey.api.audit.permissions.CanSeeAuditLogPermission; -import org.labkey.api.audit.provider.SiteSettingsAuditProvider; -import org.labkey.api.audit.view.AuditChangesView; -import org.labkey.api.data.CompareType; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerFilter; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.SimpleFilter; -import org.labkey.api.query.FieldKey; -import org.labkey.api.query.QuerySettings; -import org.labkey.api.query.QueryUrls; -import org.labkey.api.query.QueryView; -import org.labkey.api.query.UserSchema; -import org.labkey.api.security.ElevatedUser; -import org.labkey.api.security.RequiresPermission; -import org.labkey.api.security.User; -import org.labkey.api.security.permissions.AdminPermission; -import org.labkey.api.security.permissions.ReadPermission; -import org.labkey.api.security.permissions.TroubleshooterPermission; -import org.labkey.api.security.roles.ReaderRole; -import org.labkey.api.settings.AdminConsole; -import org.labkey.api.settings.LookAndFeelProperties; -import org.labkey.api.util.DateUtil; -import org.labkey.api.view.ActionURL; -import org.labkey.api.view.HttpView; -import org.labkey.api.view.JspView; -import org.labkey.api.view.NavTree; -import org.labkey.api.view.NotFoundException; -import org.labkey.api.view.VBox; -import org.springframework.validation.BindException; -import org.springframework.validation.Errors; -import org.springframework.web.servlet.ModelAndView; - -import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.Map; - -import static org.labkey.api.data.ContainerManager.REQUIRE_USER_COMMENTS_PROPERTY_NAME; - -public class AuditController extends SpringActionController -{ - private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(AuditController.class); - - public AuditController() - { - setActionResolver(_actionResolver); - } - - public static void registerAdminConsoleLinks() - { - AdminConsole.addLink(AdminConsole.SettingsLinkType.Management, "audit log", new ActionURL(ShowAuditLogAction.class, ContainerManager.getRoot()), CanSeeAuditLogPermission.class); - } - - public static class AuditUrlsImpl implements AuditUrls - { - @Override - public ActionURL getAuditLog(Container container, String eventType, @Nullable Date startDate, @Nullable Date endDate) - { - ActionURL url = new ActionURL(AuditLogAction.class, container).addParameter("eventType", eventType); - - if (startDate != null) - url.addParameter("startDate", DateUtil.toISO(startDate)); - if (endDate != null) - url.addParameter("endDate", DateUtil.toISO(endDate)); - - return url; - } - } - - @RequiresPermission(AdminPermission.class) - public class BeginAction extends SimpleRedirectAction - { - @Override - public ActionURL getRedirectURL(Object o) - { - if (getContainer() != null && getContainer().isRoot()) - return new ActionURL(ShowAuditLogAction.class, getContainer()); - else - return urlProvider(QueryUrls.class).urlSchemaBrowser(getContainer(), "auditLog"); - } - } - - // An admin console action, but we want Troubleshooters to be able to POST (for export) - @RequiresPermission(TroubleshooterPermission.class) - public static class ShowAuditLogAction extends QueryViewAction - { - public ShowAuditLogAction() - { - super(ShowAuditLogForm.class); - } - - @Override - protected ModelAndView getHtmlView(ShowAuditLogForm form, BindException errors) throws Exception - { - VBox view = new VBox(); - - JspView jspView = new JspView<>("/org/labkey/audit/auditLog.jsp", form.getView()); - - view.addView(jspView); - view.addView(createInitializedQueryView(form, errors, false, null)); - - return view; - } - - @Override - protected QueryView createQueryView(ShowAuditLogForm form, BindException errors, boolean forExport, String dataRegion) - { - // Troubleshooters don't have read permission, so add Reader as a contextual role to placate DataRegion's - // and ButtonBar's render-time permissions check. See #39638 - if (!getContainer().hasPermission(getUser(), ReadPermission.class)) - getViewContext().addContextualRole(ReaderRole.class); - - String selected = form.getView(); - - if (selected == null) - selected = AuditLogService.get().getAuditProviders().get(0).getEventName(); - - UserSchema schema = AuditLogService.getAuditLogSchema(getUser(), getContainer()); - QuerySettings settings = new QuerySettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, selected); - settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); - - return schema.createView(getViewContext(), settings, errors); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("audits"); - urlProvider(AdminUrls.class).addAdminNavTrail(root, "Audit Log", getClass(), getContainer()); - } - } - - public static class ShowAuditLogForm extends QueryViewAction.QueryExportForm - { - private String _view; - - public String getView() - { - return _view; - } - - @SuppressWarnings({"UnusedDeclaration"}) - public void setView(String view) - { - _view = view; - } - } - - - public static class SiteSettingsAuditDetailsForm - { - private Integer _id; - - public Integer getId() - { - return _id; - } - - public void setId(Integer id) - { - _id = id; - } - } - - @RequiresPermission(AdminPermission.class) - public class ShowSiteSettingsAuditDetailsAction extends SimpleViewAction - { - @Override - public ModelAndView getView(SiteSettingsAuditDetailsForm form, BindException errors) - { - if (null == form.getId() || form.getId().intValue() < 0) - throw new NotFoundException("The audit log details key was not provided!"); - - String diff = null; - User createdBy = null; - Date created = null; - - SiteSettingsAuditProvider.SiteSettingsAuditEvent event = AuditLogService.get().getAuditEvent(getUser(), SiteSettingsAuditProvider.AUDIT_EVENT_TYPE, form.getId()); - - if (event != null) - { - diff = event.getChanges(); - createdBy = event.getCreatedBy(); - created = event.getCreated(); - } - - SiteSettingsAuditDetailsModel model = new SiteSettingsAuditDetailsModel(getContainer(), diff, createdBy, created); - return new JspView<>("/org/labkey/audit/siteSettingsAuditDetails.jsp", model); - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Admin Console", urlProvider(AdminUrls.class).getAdminConsoleURL()); - - ActionURL urlLog = new ActionURL(ShowAuditLogAction.class, ContainerManager.getRoot()); - urlLog.addParameter("view", SiteSettingsAuditProvider.AUDIT_EVENT_TYPE); - root.addChild("Audit Log", urlLog); - root.addChild("Site Settings Audit Event Details"); - } - } - - @SuppressWarnings({"unused"}) - @RequiresPermission(ReadPermission.class) - public static class GetDetailedAuditChangesAction extends ReadOnlyApiAction - { - private @NotNull ContainerFilter getContainerFilter(AuditChangesForm form) throws IllegalArgumentException - { - Container container = getContainer(); - User user = getUser(); - - if (!StringUtils.isEmpty(form.getContainerFilter())) - return ContainerFilter.Type.valueOf(form.getContainerFilter()).create(container, user); - - return ContainerFilter.Type.Current.create(container, user); - } - - @Override - public Object execute(AuditChangesForm form, BindException errors) - { - ApiSimpleResponse response = new ApiSimpleResponse(); - DetailedAuditTypeEvent event = AuditLogService.get().getAuditEvent(getUser(), form.getAuditEventType(), form.getAuditRowId(), getContainerFilter(form)); - - if (event != null) - { - response.put("comment", event.getComment()); - response.put("eventUserId", event.getCreatedBy() != null ? event.getCreatedBy().getUserId() : null); - response.put("eventDateFormatted", new SimpleDateFormat(LookAndFeelProperties.getInstance(getContainer()).getDefaultDateTimeFormat()).format(event.getCreated())); - if (event.getUserComment() != null) - response.put("userComment", event.getUserComment()); - - String oldRecord = event.getOldRecordMap(); - String newRecord = event.getNewRecordMap(); - - if (oldRecord != null || newRecord != null) - { - response.put("oldData", AbstractAuditTypeProvider.decodeFromDataMap(oldRecord)); - response.put("newData", AbstractAuditTypeProvider.decodeFromDataMap(newRecord)); - } - - response.put("success", true); - return response; - } - - response.put("success", false); - return response; - } - } - - @SuppressWarnings({"unused"}) - @RequiresPermission(ReadPermission.class) - public static class DetailedAuditChangesAction extends SimpleViewAction - { - @Override - public ModelAndView getView(AuditChangesForm form, BindException errors) - { - int auditRowId = form.getAuditRowId(); - String comment = null; - String oldRecord = null; - String newRecord = null; - - DetailedAuditTypeEvent event = AuditLogService.get().getAuditEvent(getUser(), form.getAuditEventType(), auditRowId); - - if (event != null) - { - comment = event.getComment(); - oldRecord = event.getOldRecordMap(); - newRecord = event.getNewRecordMap(); - } - - if (oldRecord != null || newRecord != null) - { - Map oldData = AbstractAuditTypeProvider.decodeFromDataMap(oldRecord); - Map newData = AbstractAuditTypeProvider.decodeFromDataMap(newRecord); - - return new AuditChangesView(comment, oldData, newData); - } - return new NoRecordView(); - } - - private static class NoRecordView extends HttpView - { - @Override - protected void renderInternal(Object model, PrintWriter out) - { - out.write("

No current record found

"); - } - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Audit Details"); - } - } - - @SuppressWarnings({"unused", "WeakerAccess"}) - public static class AuditChangesForm - { - private int auditRowId; - private String auditEventType; - private String _containerFilter; - - public int getAuditRowId() - { - return auditRowId; - } - - public void setAuditRowId(int auditRowId) - { - this.auditRowId = auditRowId; - } - - public String getAuditEventType() - { - return auditEventType; - } - - public void setAuditEventType(String auditEventType) - { - this.auditEventType = auditEventType; - } - - public String getContainerFilter() - { - return _containerFilter; - } - - public void setContainerFilter(String containerFilter) - { - _containerFilter = containerFilter; - } - - } - - @RequiresPermission(ReadPermission.class) - public static class GetTransactionRowIdsAction extends ReadOnlyApiAction - { - @Override - public void validateForm(AuditTransactionForm form, Errors errors) - { - form.validate(errors); - } - - @Override - public Object execute(AuditTransactionForm form, BindException errors) - { - List rowIds; - User elevatedUser = ElevatedUser.ensureCanSeeAuditLogRole(getContainer(), getUser()); - ContainerFilter cf = ContainerFilter.getContainerFilterByName(form.getContainerFilter(), getContainer(), elevatedUser); - if (form.isSampleType()) - rowIds = AuditLogImpl.get().getTransactionSampleIds(form.getTransactionAuditId(), elevatedUser, getContainer(), cf); - else - rowIds = AuditLogImpl.get().getTransactionSourceIds(form.getTransactionAuditId(), elevatedUser, getContainer(), cf); - - ApiSimpleResponse response = new ApiSimpleResponse(); - response.put("success", true); - response.put("rowIds", rowIds); - - return response; - } - } - - public static class AuditTransactionForm - { - private Long _transactionAuditId; - private String _dataType; - private boolean _isSampleType; - String _containerFilter; - - public Long getTransactionAuditId() - { - return _transactionAuditId; - } - - public void setTransactionAuditId(Long transactionAuditId) - { - _transactionAuditId = transactionAuditId; - } - - public String getDataType() - { - return _dataType; - } - - public void setDataType(String dataType) - { - _dataType = dataType; - } - - public boolean isSampleType() - { - return _isSampleType; - } - - public String getContainerFilter() - { - return _containerFilter; - } - - public void setContainerFilter(String containerFilter) - { - _containerFilter = containerFilter; - } - - public void validate(Errors errors) - { - if (getTransactionAuditId() == null) - errors.reject(ERROR_REQUIRED, "'transactionAuditId' is required"); - if (getDataType() == null) - errors.reject(ERROR_REQUIRED, "'dataType' is required"); - else - { - _isSampleType = getDataType().equalsIgnoreCase("samples"); - if (!_isSampleType && !getDataType().equalsIgnoreCase("sources")) - errors.reject(ERROR_MSG, "Unknown dataType: " + getDataType()); - } - } - } - - public static class AuditLogForm - { - private String _eventType; - private Date _startDate; - private Date _endDate; - - public String getEventType() - { - return _eventType; - } - - public void setEventType(String eventType) - { - _eventType = eventType; - } - - public Date getStartDate() - { - return _startDate; - } - - public void setStartDate(Date startDate) - { - _startDate = startDate; - } - - public Date getEndDate() - { - return _endDate; - } - - public void setEndDate(Date endDate) - { - _endDate = endDate; - } - } - - @RequiresPermission(CanSeeAuditLogPermission.class) - public static class AuditLogAction extends SimpleViewAction - { - private String _eventType; - - @Override - public ModelAndView getView(AuditLogForm form, BindException errors) throws Exception - { - _eventType = form.getEventType(); - - UserSchema schema = AuditLogService.getAuditLogSchema(getUser(), getContainer()); - QuerySettings settings = new QuerySettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, _eventType); - - SimpleFilter filter = new SimpleFilter(); - if (form.getStartDate() != null) - { - Calendar c = new GregorianCalendar(); - c.setTime(form.getStartDate()); - filter.addCondition(FieldKey.fromParts("created"), c, CompareType.DATE_GTE); - } - - if (form.getEndDate() != null) - { - Calendar c = new GregorianCalendar(); - c.setTime(form.getEndDate()); - filter.addCondition(FieldKey.fromParts("created"), c, CompareType.DATE_LTE); - } - - // add additional filters that may be on the URL - filter.addUrlFilters(getViewContext().getActionURL(), QueryView.DATAREGIONNAME_DEFAULT); - settings.setBaseFilter(filter); - - return schema.createView(getViewContext(), settings, errors); - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild(_eventType + " : Audit Log"); - } - } - - @RequiresPermission(AdminPermission.class) - public static class SaveAuditSettingsAction extends MutatingApiAction - { - @Override - public void validateForm(AuditSettingsForm form, Errors errors) - { - if (!getContainer().isAppHomeFolder()) - errors.reject(ERROR_GENERIC, "This action is not supported for sub-folders of the application."); - if (form.getRequireUserComments() == null) - errors.reject(ERROR_REQUIRED, "requireUserComments is required to be non-null."); - } - - @Override - public Object execute(AuditSettingsForm form, BindException errors) - { - ContainerManager.setRequireAuditComments(getContainer(), getUser(), form.getRequireUserComments()); - return success(); - } - } - - public static class AuditSettingsForm - { - private Boolean _requireUserComments; - - public Boolean getRequireUserComments() - { - return _requireUserComments; - } - - public void setRequireUserComments(Boolean requireUserComments) - { - _requireUserComments = requireUserComments; - } - } - - @RequiresPermission(ReadPermission.class) - public static class GetAuditSettingsAction extends ReadOnlyApiAction - { - @Override - public Object execute(Object o, BindException errors) - { - Container container = getContainer(); - if (!container.isAppHomeFolder()) - container = container.getProject(); - return container == null ? Collections.emptyMap() : Map.of(REQUIRE_USER_COMMENTS_PROPERTY_NAME, container.getAuditCommentsRequired()); - } - } -} +/* + * 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.audit; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.action.ApiSimpleResponse; +import org.labkey.api.action.MutatingApiAction; +import org.labkey.api.action.QueryViewAction; +import org.labkey.api.action.ReadOnlyApiAction; +import org.labkey.api.action.SimpleRedirectAction; +import org.labkey.api.action.SimpleViewAction; +import org.labkey.api.action.SpringActionController; +import org.labkey.api.admin.AdminUrls; +import org.labkey.api.audit.AbstractAuditTypeProvider; +import org.labkey.api.audit.AuditLogService; +import org.labkey.api.audit.AuditUrls; +import org.labkey.api.audit.DetailedAuditTypeEvent; +import org.labkey.api.audit.permissions.CanSeeAuditLogPermission; +import org.labkey.api.audit.provider.SiteSettingsAuditProvider; +import org.labkey.api.audit.view.AuditChangesView; +import org.labkey.api.data.CompareType; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.QuerySettings; +import org.labkey.api.query.QueryUrls; +import org.labkey.api.query.QueryView; +import org.labkey.api.query.UserSchema; +import org.labkey.api.security.ElevatedUser; +import org.labkey.api.security.RequiresPermission; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.security.permissions.TroubleshooterPermission; +import org.labkey.api.security.roles.ReaderRole; +import org.labkey.api.settings.AdminConsole; +import org.labkey.api.settings.LookAndFeelProperties; +import org.labkey.api.util.DateUtil; +import org.labkey.api.util.Pair; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.HttpView; +import org.labkey.api.view.JspView; +import org.labkey.api.view.NavTree; +import org.labkey.api.view.NotFoundException; +import org.labkey.api.view.VBox; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.servlet.ModelAndView; + +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Map; + +import static org.labkey.api.data.ContainerManager.REQUIRE_USER_COMMENTS_PROPERTY_NAME; + +public class AuditController extends SpringActionController +{ + private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(AuditController.class); + + public AuditController() + { + setActionResolver(_actionResolver); + } + + public static void registerAdminConsoleLinks() + { + AdminConsole.addLink(AdminConsole.SettingsLinkType.Management, "audit log", new ActionURL(ShowAuditLogAction.class, ContainerManager.getRoot()), CanSeeAuditLogPermission.class); + } + + public static class AuditUrlsImpl implements AuditUrls + { + @Override + public ActionURL getAuditLog(Container container, String eventType, @Nullable Date startDate, @Nullable Date endDate) + { + ActionURL url = new ActionURL(AuditLogAction.class, container).addParameter("eventType", eventType); + + if (startDate != null) + url.addParameter("startDate", DateUtil.toISO(startDate)); + if (endDate != null) + url.addParameter("endDate", DateUtil.toISO(endDate)); + + return url; + } + } + + @RequiresPermission(AdminPermission.class) + public class BeginAction extends SimpleRedirectAction + { + @Override + public ActionURL getRedirectURL(Object o) + { + if (getContainer() != null && getContainer().isRoot()) + return new ActionURL(ShowAuditLogAction.class, getContainer()); + else + return urlProvider(QueryUrls.class).urlSchemaBrowser(getContainer(), "auditLog"); + } + } + + // An admin console action, but we want Troubleshooters to be able to POST (for export) + @RequiresPermission(TroubleshooterPermission.class) + public static class ShowAuditLogAction extends QueryViewAction + { + public ShowAuditLogAction() + { + super(ShowAuditLogForm.class); + } + + @Override + protected ModelAndView getHtmlView(ShowAuditLogForm form, BindException errors) throws Exception + { + VBox view = new VBox(); + + JspView jspView = new JspView<>("/org/labkey/audit/auditLog.jsp", form.getView()); + + view.addView(jspView); + view.addView(createInitializedQueryView(form, errors, false, null)); + + return view; + } + + @Override + protected QueryView createQueryView(ShowAuditLogForm form, BindException errors, boolean forExport, String dataRegion) + { + // Troubleshooters don't have read permission, so add Reader as a contextual role to placate DataRegion's + // and ButtonBar's render-time permissions check. See #39638 + if (!getContainer().hasPermission(getUser(), ReadPermission.class)) + getViewContext().addContextualRole(ReaderRole.class); + + String selected = form.getView(); + + if (selected == null) + selected = AuditLogService.get().getAuditProviders().get(0).getEventName(); + + UserSchema schema = AuditLogService.getAuditLogSchema(getUser(), getContainer()); + QuerySettings settings = new QuerySettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, selected); + settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); + + return schema.createView(getViewContext(), settings, errors); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("audits"); + urlProvider(AdminUrls.class).addAdminNavTrail(root, "Audit Log", getClass(), getContainer()); + } + } + + public static class ShowAuditLogForm extends QueryViewAction.QueryExportForm + { + private String _view; + + public String getView() + { + return _view; + } + + @SuppressWarnings({"UnusedDeclaration"}) + public void setView(String view) + { + _view = view; + } + } + + + public static class SiteSettingsAuditDetailsForm + { + private Integer _id; + + public Integer getId() + { + return _id; + } + + public void setId(Integer id) + { + _id = id; + } + } + + @RequiresPermission(AdminPermission.class) + public class ShowSiteSettingsAuditDetailsAction extends SimpleViewAction + { + @Override + public ModelAndView getView(SiteSettingsAuditDetailsForm form, BindException errors) + { + if (null == form.getId() || form.getId().intValue() < 0) + throw new NotFoundException("The audit log details key was not provided!"); + + String diff = null; + User createdBy = null; + Date created = null; + + SiteSettingsAuditProvider.SiteSettingsAuditEvent event = AuditLogService.get().getAuditEvent(getUser(), SiteSettingsAuditProvider.AUDIT_EVENT_TYPE, form.getId()); + + if (event != null) + { + diff = event.getChanges(); + createdBy = event.getCreatedBy(); + created = event.getCreated(); + } + + SiteSettingsAuditDetailsModel model = new SiteSettingsAuditDetailsModel(getContainer(), diff, createdBy, created); + return new JspView<>("/org/labkey/audit/siteSettingsAuditDetails.jsp", model); + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Admin Console", urlProvider(AdminUrls.class).getAdminConsoleURL()); + + ActionURL urlLog = new ActionURL(ShowAuditLogAction.class, ContainerManager.getRoot()); + urlLog.addParameter("view", SiteSettingsAuditProvider.AUDIT_EVENT_TYPE); + root.addChild("Audit Log", urlLog); + root.addChild("Site Settings Audit Event Details"); + } + } + + @SuppressWarnings({"unused"}) + @RequiresPermission(ReadPermission.class) + public static class GetDetailedAuditChangesAction extends ReadOnlyApiAction + { + private @NotNull ContainerFilter getContainerFilter(AuditChangesForm form) throws IllegalArgumentException + { + Container container = getContainer(); + User user = getUser(); + + if (!StringUtils.isEmpty(form.getContainerFilter())) + return ContainerFilter.Type.valueOf(form.getContainerFilter()).create(container, user); + + return ContainerFilter.Type.Current.create(container, user); + } + + @Override + public Object execute(AuditChangesForm form, BindException errors) + { + ApiSimpleResponse response = new ApiSimpleResponse(); + DetailedAuditTypeEvent event = AuditLogService.get().getAuditEvent(getUser(), form.getAuditEventType(), form.getAuditRowId(), getContainerFilter(form)); + + if (event != null) + { + response.put("comment", event.getComment()); + response.put("eventUserId", event.getCreatedBy() != null ? event.getCreatedBy().getUserId() : null); + response.put("eventDateFormatted", new SimpleDateFormat(LookAndFeelProperties.getInstance(getContainer()).getDefaultDateTimeFormat()).format(event.getCreated())); + if (event.getUserComment() != null) + response.put("userComment", event.getUserComment()); + + String oldRecord = event.getOldRecordMap(); + String newRecord = event.getNewRecordMap(); + + if (oldRecord != null || newRecord != null) + { + response.put("oldData", AbstractAuditTypeProvider.decodeFromDataMap(oldRecord)); + response.put("newData", AbstractAuditTypeProvider.decodeFromDataMap(newRecord)); + } + + response.put("success", true); + return response; + } + + response.put("success", false); + return response; + } + } + + @SuppressWarnings({"unused"}) + @RequiresPermission(ReadPermission.class) + public static class DetailedAuditChangesAction extends SimpleViewAction + { + @Override + public ModelAndView getView(AuditChangesForm form, BindException errors) + { + int auditRowId = form.getAuditRowId(); + String comment = null; + String oldRecord = null; + String newRecord = null; + + DetailedAuditTypeEvent event = AuditLogService.get().getAuditEvent(getUser(), form.getAuditEventType(), auditRowId); + + if (event != null) + { + comment = event.getComment(); + oldRecord = event.getOldRecordMap(); + newRecord = event.getNewRecordMap(); + } + + if (oldRecord != null || newRecord != null) + { + Map oldData = AbstractAuditTypeProvider.decodeFromDataMap(oldRecord); + Map newData = AbstractAuditTypeProvider.decodeFromDataMap(newRecord); + + return new AuditChangesView(comment, oldData, newData); + } + return new NoRecordView(); + } + + private static class NoRecordView extends HttpView + { + @Override + protected void renderInternal(Object model, PrintWriter out) + { + out.write("

No current record found

"); + } + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Audit Details"); + } + } + + @SuppressWarnings({"unused", "WeakerAccess"}) + public static class AuditChangesForm + { + private int auditRowId; + private String auditEventType; + private String _containerFilter; + + public int getAuditRowId() + { + return auditRowId; + } + + public void setAuditRowId(int auditRowId) + { + this.auditRowId = auditRowId; + } + + public String getAuditEventType() + { + return auditEventType; + } + + public void setAuditEventType(String auditEventType) + { + this.auditEventType = auditEventType; + } + + public String getContainerFilter() + { + return _containerFilter; + } + + public void setContainerFilter(String containerFilter) + { + _containerFilter = containerFilter; + } + + } + + @RequiresPermission(ReadPermission.class) + public static class GetTransactionRowIdsAction extends ReadOnlyApiAction + { + @Override + public void validateForm(AuditTransactionForm form, Errors errors) + { + form.validate(errors); + } + + @Override + public Object execute(AuditTransactionForm form, BindException errors) + { + Pair, Map> results; + User elevatedUser = ElevatedUser.ensureCanSeeAuditLogRole(getContainer(), getUser()); + ContainerFilter cf = ContainerFilter.getContainerFilterByName(form.getContainerFilter(), getContainer(), elevatedUser); + if (form.isSampleType()) + results = AuditLogImpl.get().getTransactionSampleIds(form.getTransactionAuditId(), elevatedUser, getContainer(), cf); + else + results = AuditLogImpl.get().getTransactionSourceIds(form.getTransactionAuditId(), elevatedUser, getContainer(), cf); + + ApiSimpleResponse response = new ApiSimpleResponse(); + response.put("success", true); + response.put("rowIds", results.first); + response.put("dataTypeIds", results.second); + + return response; + } + } + + public static class AuditTransactionForm + { + private Long _transactionAuditId; + private String _dataType; + private boolean _isSampleType; + String _containerFilter; + + public Long getTransactionAuditId() + { + return _transactionAuditId; + } + + public void setTransactionAuditId(Long transactionAuditId) + { + _transactionAuditId = transactionAuditId; + } + + public String getDataType() + { + return _dataType; + } + + public void setDataType(String dataType) + { + _dataType = dataType; + } + + public boolean isSampleType() + { + return _isSampleType; + } + + public String getContainerFilter() + { + return _containerFilter; + } + + public void setContainerFilter(String containerFilter) + { + _containerFilter = containerFilter; + } + + public void validate(Errors errors) + { + if (getTransactionAuditId() == null) + errors.reject(ERROR_REQUIRED, "'transactionAuditId' is required"); + if (getDataType() == null) + errors.reject(ERROR_REQUIRED, "'dataType' is required"); + else + { + _isSampleType = getDataType().equalsIgnoreCase("samples"); + if (!_isSampleType && !getDataType().equalsIgnoreCase("sources")) + errors.reject(ERROR_MSG, "Unknown dataType: " + getDataType()); + } + } + } + + public static class AuditLogForm + { + private String _eventType; + private Date _startDate; + private Date _endDate; + + public String getEventType() + { + return _eventType; + } + + public void setEventType(String eventType) + { + _eventType = eventType; + } + + public Date getStartDate() + { + return _startDate; + } + + public void setStartDate(Date startDate) + { + _startDate = startDate; + } + + public Date getEndDate() + { + return _endDate; + } + + public void setEndDate(Date endDate) + { + _endDate = endDate; + } + } + + @RequiresPermission(CanSeeAuditLogPermission.class) + public static class AuditLogAction extends SimpleViewAction + { + private String _eventType; + + @Override + public ModelAndView getView(AuditLogForm form, BindException errors) throws Exception + { + _eventType = form.getEventType(); + + UserSchema schema = AuditLogService.getAuditLogSchema(getUser(), getContainer()); + QuerySettings settings = new QuerySettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, _eventType); + + SimpleFilter filter = new SimpleFilter(); + if (form.getStartDate() != null) + { + Calendar c = new GregorianCalendar(); + c.setTime(form.getStartDate()); + filter.addCondition(FieldKey.fromParts("created"), c, CompareType.DATE_GTE); + } + + if (form.getEndDate() != null) + { + Calendar c = new GregorianCalendar(); + c.setTime(form.getEndDate()); + filter.addCondition(FieldKey.fromParts("created"), c, CompareType.DATE_LTE); + } + + // add additional filters that may be on the URL + filter.addUrlFilters(getViewContext().getActionURL(), QueryView.DATAREGIONNAME_DEFAULT); + settings.setBaseFilter(filter); + + return schema.createView(getViewContext(), settings, errors); + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild(_eventType + " : Audit Log"); + } + } + + @RequiresPermission(AdminPermission.class) + public static class SaveAuditSettingsAction extends MutatingApiAction + { + @Override + public void validateForm(AuditSettingsForm form, Errors errors) + { + if (!getContainer().isAppHomeFolder()) + errors.reject(ERROR_GENERIC, "This action is not supported for sub-folders of the application."); + if (form.getRequireUserComments() == null) + errors.reject(ERROR_REQUIRED, "requireUserComments is required to be non-null."); + } + + @Override + public Object execute(AuditSettingsForm form, BindException errors) + { + ContainerManager.setRequireAuditComments(getContainer(), getUser(), form.getRequireUserComments()); + return success(); + } + } + + public static class AuditSettingsForm + { + private Boolean _requireUserComments; + + public Boolean getRequireUserComments() + { + return _requireUserComments; + } + + public void setRequireUserComments(Boolean requireUserComments) + { + _requireUserComments = requireUserComments; + } + } + + @RequiresPermission(ReadPermission.class) + public static class GetAuditSettingsAction extends ReadOnlyApiAction + { + @Override + public Object execute(Object o, BindException errors) + { + Container container = getContainer(); + if (!container.isAppHomeFolder()) + container = container.getProject(); + return container == null ? Collections.emptyMap() : Map.of(REQUIRE_USER_COMMENTS_PROPERTY_NAME, container.getAuditCommentsRequired()); + } + } +} diff --git a/audit/src/org/labkey/audit/AuditLogImpl.java b/audit/src/org/labkey/audit/AuditLogImpl.java index dd38dba1fbe..3c0b6479b6e 100644 --- a/audit/src/org/labkey/audit/AuditLogImpl.java +++ b/audit/src/org/labkey/audit/AuditLogImpl.java @@ -1,316 +1,315 @@ -/* - * 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.audit; - -import jakarta.servlet.ServletContext; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.action.SpringActionController; -import org.labkey.api.audit.AbstractAuditTypeProvider; -import org.labkey.api.audit.AuditLogService; -import org.labkey.api.audit.AuditTypeEvent; -import org.labkey.api.audit.AuditTypeProvider; -import org.labkey.api.audit.DetailedAuditTypeEvent; -import org.labkey.api.audit.SampleTimelineAuditEvent; -import org.labkey.api.cache.Cache; -import org.labkey.api.cache.CacheManager; -import org.labkey.api.collections.CaseInsensitiveHashMap; -import org.labkey.api.data.CompareType; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerFilter; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.SimpleFilter; -import org.labkey.api.data.Sort; -import org.labkey.api.data.TableSelector; -import org.labkey.api.exp.api.ExperimentService; -import org.labkey.api.module.ModuleLoader; -import org.labkey.api.query.FieldKey; -import org.labkey.api.query.QueryService; -import org.labkey.api.query.UserSchema; -import org.labkey.api.security.User; -import org.labkey.api.security.UserManager; -import org.labkey.api.util.ContextListener; -import org.labkey.api.util.Pair; -import org.labkey.api.util.StartupListener; -import org.labkey.api.util.logging.LogHelper; -import org.labkey.api.view.ActionURL; -import org.labkey.api.view.HttpView; -import org.labkey.audit.model.LogManager; -import org.labkey.audit.query.AuditQuerySchema; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - -public class AuditLogImpl implements AuditLogService, StartupListener -{ - private static final AuditLogImpl _instance = new AuditLogImpl(); - - private static final Logger _log = LogHelper.getLogger(AuditLogImpl.class, "Audit service interactions."); - - private final Queue> _eventTypeQueue = new LinkedList<>(); - private final AtomicBoolean _logToDatabase = new AtomicBoolean(false); - private static final Object STARTUP_LOCK = new Object(); - - // Cache the audit events associated with transaction ids. We currently use these for interacting with objects - // that were created immediately after they were created, so the cache size does not need to be very large and the defaultTimeToLive can be small. - // Use a pair as the cache object to avoid warnings about mutable cache objects (Issue 48779). - // Since this is all about capturing data from the same transaction, there shouldn't be other threads in the mix. - private static final Cache>> TRANSACTION_EVENT_CACHE = CacheManager.getBlockingCache(50, CacheManager.HOUR, - "Transaction Audit Event Cache", - (key, argument) -> Pair.of(key, new ArrayList<>()) - ); - - public static AuditLogImpl get() - { - return _instance; - } - - private AuditLogImpl() - { - // If we're migrating, avoid creating all the audit log tables and inserting the queued events - if (ModuleLoader.getInstance().shouldInsertData()) - ContextListener.addStartupListener(this); - } - - @Override - public String getName() - { - return "Audit Log"; - } - - @Override - public void moduleStartupComplete(ServletContext servletContext) - { - // perform audit provider initialization - for (AuditTypeProvider provider : AuditLogService.get().getAuditProviders()) - { - provider.initializeProvider(User.getAdminServiceUser()); - } - - // Synchronize so that we can guarantee that all events have already been added to the queue before we - // start processing them - synchronized (STARTUP_LOCK) - { - _logToDatabase.set(true); - } - - while (!_eventTypeQueue.isEmpty()) - { - Pair event = _eventTypeQueue.remove(); - addEvents(event.first, List.of(event.second)); - } - } - - @Override - public boolean isViewable() - { - return true; - } - - @Override - public K addEvent(User user, K event) - { - return _addEvents(user, List.of(event),true, false); - } - - @Override - public void addEvents(@Nullable User user, List events) - { - _addEvents(user, events, false, false); - } - - @Override - public void addEvents(@Nullable User user, List events, boolean useTransactionAuditCache) - { - _addEvents(user, events, false, useTransactionAuditCache); - } - - private K _addEvents(@Nullable User user, List events, boolean reselectEvent, boolean useTransactionAuditCache) - { - assert !reselectEvent || events.size() == 1; - - for (var event : events) - { - assert event.getContainer() != null : "Container cannot be null"; - - if (user == null) - { - if (HttpView.hasCurrentView() && HttpView.currentContext() != null) - _log.warn("user was not specified for event type " + event.getEventType() + " in container " + event.getContainer() + "; defaulting to guest user."); - user = UserManager.getGuestUser(); - } - if (event.getTransactionId() != null && useTransactionAuditCache) - { - List transactionEvents = TRANSACTION_EVENT_CACHE.get(event.getTransactionId()).second; - transactionEvents.add(event); - } - - if (event.getImpersonatedBy() == null && user.isImpersonated()) - { - User impersonatingUser = user.getImpersonatingUser(); - event.setImpersonatedBy(impersonatingUser.getUserId()); - } - } - - try (var ignored = SpringActionController.ignoreSqlUpdates()) - { - /* - This is necessary because audit log service needs to be registered in the constructor - of the audit module, but the schema may not be created or updated at that point. Events - that occur before startup is complete are therefore queued up and recorded after startup. - */ - boolean databaseReady; - synchronized (STARTUP_LOCK) - { - // Keep the critical section as lean as possible - just guarantee that all the events - // have been queued before releasing the lock - databaseReady = _logToDatabase.get(); - if (!databaseReady) - { - for (var event : events) - _eventTypeQueue.add(new Pair<>(user, event)); - } - } - - if (databaseReady) - { - if (reselectEvent && events.size()==1) - return LogManager.get().insertEvent(user, events.get(0)); - LogManager.get().insertEvents(user, events); - } - } - catch (RuntimeException e) - { - _log.error("Failed to insert audit log event", e); - AuditLogService.handleAuditFailure(user, e); - throw e; - } - return null; - } - - @Override - public UserSchema createSchema(User user, Container container) - { - return new AuditQuerySchema(user, container); - } - - @Nullable - @Override - public K getAuditEvent(User user, String eventType, int rowId) - { - return LogManager.get().getAuditEvent(user, eventType, rowId); - } - - @Nullable - @Override - public K getAuditEvent(User user, String eventType, int rowId, @Nullable ContainerFilter cf) - { - return LogManager.get().getAuditEvent(user, eventType, rowId, cf); - } - - @Override - public List getAuditEvents(Container container, User user, String eventType, @Nullable SimpleFilter filter, @Nullable Sort sort) - { - return LogManager.get().getAuditEvents(container, user, eventType, filter, sort); - } - - @Override - public List getAuditEvents(Container container, User user, String eventType, @Nullable SimpleFilter filter, @Nullable Sort sort, @Nullable ContainerFilter cf) - { - return LogManager.get().getAuditEvents(container, user, eventType, filter, sort, cf); - } - - @Override - public ActionURL getAuditUrl() - { - return new ActionURL(AuditController.ShowAuditLogAction.class, ContainerManager.getRoot()); - } - - public List getTransactionSampleIds(long transactionAuditId, User user, Container container, @Nullable ContainerFilter containerFilter) - { - List transactionEvents = TRANSACTION_EVENT_CACHE.get(transactionAuditId).second; - if (!transactionEvents.isEmpty()) - { - List ids = new ArrayList<>(); - transactionEvents.forEach(event -> { - if (event instanceof SampleTimelineAuditEvent stEvent) - ids.add(stEvent.getSampleId()); - }); - return ids; - } - - SimpleFilter filter = new SimpleFilter(); - filter.addCondition(FieldKey.fromParts("TransactionID"), transactionAuditId); - - List events = AuditLogService.get().getAuditEvents(container, user, SampleTimelineAuditEvent.EVENT_TYPE, filter, null, containerFilter); - return events.stream().map(SampleTimelineAuditEvent::getSampleId).collect(Collectors.toList()); - } - - public List getTransactionSourceIds(long transactionAuditId, User user, Container container, @Nullable ContainerFilter containerFilter) - { - List lsids = new ArrayList<>(); - List sourceIds = new ArrayList<>(); - List transactionEvents = TRANSACTION_EVENT_CACHE.get(transactionAuditId).second; - if (!transactionEvents.isEmpty()) - { - transactionEvents.forEach(event -> { - if (event instanceof DetailedAuditTypeEvent detailedEvent) - { - if (detailedEvent.getNewRecordMap() != null) - { - Map newRecord = new CaseInsensitiveHashMap<>(AbstractAuditTypeProvider.decodeFromDataMap(detailedEvent.getNewRecordMap())); - if (newRecord.containsKey("RowId") && !StringUtils.isEmpty(newRecord.get("RowId"))) - sourceIds.add(Long.valueOf(newRecord.get("RowId"))); - else if (newRecord.containsKey("LSID") && !StringUtils.isEmpty(newRecord.get("LSID"))) - lsids.add(newRecord.get("LSID")); - } - } - }); - } - else - { - List events = QueryService.get().getQueryUpdateAuditRecords(user, container, transactionAuditId, containerFilter); - - events.forEach((event) -> { - if (event.getNewRecordMap() != null) - { - Map newRecord = new CaseInsensitiveHashMap<>(AbstractAuditTypeProvider.decodeFromDataMap(event.getNewRecordMap())); - if (newRecord.containsKey("RowId") && !StringUtils.isEmpty(newRecord.get("RowId"))) - sourceIds.add(Long.valueOf(newRecord.get("RowId"))); - else if (newRecord.containsKey("LSID") && !StringUtils.isEmpty(newRecord.get("LSID"))) - lsids.add(newRecord.get("LSID")); - - } - }); - } - if (!lsids.isEmpty()) - { - SimpleFilter filter = SimpleFilter.createContainerFilter(container); - filter.addCondition(FieldKey.fromParts("LSID"), lsids, CompareType.IN); - TableSelector selector = new TableSelector(ExperimentService.get().getTinfoData(), Collections.singleton("RowId"), filter, null); - sourceIds.addAll(selector.getArrayList(Long.class)); - } - return sourceIds; - } -} +/* + * 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.audit; + +import jakarta.servlet.ServletContext; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.action.SpringActionController; +import org.labkey.api.audit.AbstractAuditTypeProvider; +import org.labkey.api.audit.AuditLogService; +import org.labkey.api.audit.AuditTypeEvent; +import org.labkey.api.audit.AuditTypeProvider; +import org.labkey.api.audit.DetailedAuditTypeEvent; +import org.labkey.api.audit.SampleTimelineAuditEvent; +import org.labkey.api.cache.Cache; +import org.labkey.api.cache.CacheManager; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.CompareType; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Sort; +import org.labkey.api.data.TableSelector; +import org.labkey.api.exp.api.ExperimentService; +import org.labkey.api.module.ModuleLoader; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.UserSchema; +import org.labkey.api.security.User; +import org.labkey.api.security.UserManager; +import org.labkey.api.util.ContextListener; +import org.labkey.api.util.Pair; +import org.labkey.api.util.StartupListener; +import org.labkey.api.util.logging.LogHelper; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.HttpView; +import org.labkey.audit.model.LogManager; +import org.labkey.audit.query.AuditQuerySchema; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +public class AuditLogImpl implements AuditLogService, StartupListener +{ + private static final AuditLogImpl _instance = new AuditLogImpl(); + + private static final Logger _log = LogHelper.getLogger(AuditLogImpl.class, "Audit service interactions."); + + private final Queue> _eventTypeQueue = new LinkedList<>(); + private final AtomicBoolean _logToDatabase = new AtomicBoolean(false); + private static final Object STARTUP_LOCK = new Object(); + + // Cache the audit events associated with transaction ids. We currently use these for interacting with objects + // that were created immediately after they were created, so the cache size does not need to be very large and the defaultTimeToLive can be small. + // Use a pair as the cache object to avoid warnings about mutable cache objects (Issue 48779). + // Since this is all about capturing data from the same transaction, there shouldn't be other threads in the mix. + private static final Cache>> TRANSACTION_EVENT_CACHE = CacheManager.getBlockingCache(50, CacheManager.HOUR, + "Transaction Audit Event Cache", + (key, argument) -> Pair.of(key, new ArrayList<>()) + ); + + public static AuditLogImpl get() + { + return _instance; + } + + private AuditLogImpl() + { + // If we're migrating, avoid creating all the audit log tables and inserting the queued events + if (ModuleLoader.getInstance().shouldInsertData()) + ContextListener.addStartupListener(this); + } + + @Override + public String getName() + { + return "Audit Log"; + } + + @Override + public void moduleStartupComplete(ServletContext servletContext) + { + // perform audit provider initialization + for (AuditTypeProvider provider : AuditLogService.get().getAuditProviders()) + { + provider.initializeProvider(User.getAdminServiceUser()); + } + + // Synchronize so that we can guarantee that all events have already been added to the queue before we + // start processing them + synchronized (STARTUP_LOCK) + { + _logToDatabase.set(true); + } + + while (!_eventTypeQueue.isEmpty()) + { + Pair event = _eventTypeQueue.remove(); + addEvents(event.first, List.of(event.second)); + } + } + + @Override + public boolean isViewable() + { + return true; + } + + @Override + public K addEvent(User user, K event) + { + return _addEvents(user, List.of(event),true, false); + } + + @Override + public void addEvents(@Nullable User user, List events) + { + _addEvents(user, events, false, false); + } + + @Override + public void addEvents(@Nullable User user, List events, boolean useTransactionAuditCache) + { + _addEvents(user, events, false, useTransactionAuditCache); + } + + private K _addEvents(@Nullable User user, List events, boolean reselectEvent, boolean useTransactionAuditCache) + { + assert !reselectEvent || events.size() == 1; + + for (var event : events) + { + assert event.getContainer() != null : "Container cannot be null"; + + if (user == null) + { + if (HttpView.hasCurrentView() && HttpView.currentContext() != null) + _log.warn("user was not specified for event type " + event.getEventType() + " in container " + event.getContainer() + "; defaulting to guest user."); + user = UserManager.getGuestUser(); + } + if (event.getTransactionId() != null && useTransactionAuditCache) + { + List transactionEvents = TRANSACTION_EVENT_CACHE.get(event.getTransactionId()).second; + transactionEvents.add(event); + } + + if (event.getImpersonatedBy() == null && user.isImpersonated()) + { + User impersonatingUser = user.getImpersonatingUser(); + event.setImpersonatedBy(impersonatingUser.getUserId()); + } + } + + try (var ignored = SpringActionController.ignoreSqlUpdates()) + { + /* + This is necessary because audit log service needs to be registered in the constructor + of the audit module, but the schema may not be created or updated at that point. Events + that occur before startup is complete are therefore queued up and recorded after startup. + */ + boolean databaseReady; + synchronized (STARTUP_LOCK) + { + // Keep the critical section as lean as possible - just guarantee that all the events + // have been queued before releasing the lock + databaseReady = _logToDatabase.get(); + if (!databaseReady) + { + for (var event : events) + _eventTypeQueue.add(new Pair<>(user, event)); + } + } + + if (databaseReady) + { + if (reselectEvent && events.size()==1) + return LogManager.get().insertEvent(user, events.get(0)); + LogManager.get().insertEvents(user, events); + } + } + catch (RuntimeException e) + { + _log.error("Failed to insert audit log event", e); + AuditLogService.handleAuditFailure(user, e); + throw e; + } + return null; + } + + @Override + public UserSchema createSchema(User user, Container container) + { + return new AuditQuerySchema(user, container); + } + + @Nullable + @Override + public K getAuditEvent(User user, String eventType, int rowId) + { + return LogManager.get().getAuditEvent(user, eventType, rowId); + } + + @Nullable + @Override + public K getAuditEvent(User user, String eventType, int rowId, @Nullable ContainerFilter cf) + { + return LogManager.get().getAuditEvent(user, eventType, rowId, cf); + } + + @Override + public List getAuditEvents(Container container, User user, String eventType, @Nullable SimpleFilter filter, @Nullable Sort sort) + { + return LogManager.get().getAuditEvents(container, user, eventType, filter, sort); + } + + @Override + public List getAuditEvents(Container container, User user, String eventType, @Nullable SimpleFilter filter, @Nullable Sort sort, @Nullable ContainerFilter cf) + { + return LogManager.get().getAuditEvents(container, user, eventType, filter, sort, cf); + } + + @Override + public ActionURL getAuditUrl() + { + return new ActionURL(AuditController.ShowAuditLogAction.class, ContainerManager.getRoot()); + } + + public Pair, Map> getTransactionSampleIds(long transactionAuditId, User user, Container container, @Nullable ContainerFilter containerFilter) + { + List transactionEvents = TRANSACTION_EVENT_CACHE.get(transactionAuditId).second; + List events; + if (transactionEvents.isEmpty()) + { + SimpleFilter filter = new SimpleFilter(); + filter.addCondition(FieldKey.fromParts("TransactionID"), transactionAuditId); + events = AuditLogService.get().getAuditEvents(container, user, SampleTimelineAuditEvent.EVENT_TYPE, filter, null, containerFilter); + } + else + { + events = transactionEvents.stream() + .filter(SampleTimelineAuditEvent.class::isInstance) + .map(SampleTimelineAuditEvent.class::cast) + .toList(); + } + Map dataTypeRowCounts = new HashMap<>(); + List sampleIds = new ArrayList<>(); + events.forEach(event -> { + dataTypeRowCounts.merge(event.getSampleTypeId(), 1L, Long::sum); + sampleIds.add(event.getSampleId()); + }); + return Pair.of(sampleIds, dataTypeRowCounts); + } + + public Pair, Map> getTransactionSourceIds(long transactionAuditId, User user, Container container, @Nullable ContainerFilter containerFilter) + { + List lsids = new ArrayList<>(); + List sourceIds = new ArrayList<>(); + Map dataTypeRowCounts = new HashMap<>(); + List transactionEvents = TRANSACTION_EVENT_CACHE.get(transactionAuditId).second; + List detailedEvents = transactionEvents.isEmpty() + ? QueryService.get().getQueryUpdateAuditRecords(user, container, transactionAuditId, containerFilter) + : transactionEvents.stream() + .filter(DetailedAuditTypeEvent.class::isInstance) + .map(DetailedAuditTypeEvent.class::cast) + .toList(); + + detailedEvents.forEach(event -> { + if (event.getNewRecordMap() != null) + { + Map newRecord = new CaseInsensitiveHashMap<>(AbstractAuditTypeProvider.decodeFromDataMap(event.getNewRecordMap())); + if (newRecord.containsKey("RowId") && !StringUtils.isEmpty(newRecord.get("RowId"))) + sourceIds.add(Long.valueOf(newRecord.get("RowId"))); + else if (newRecord.containsKey("LSID") && !StringUtils.isEmpty(newRecord.get("LSID"))) + lsids.add(newRecord.get("LSID")); + + if (newRecord.containsKey("ClassId") && !StringUtils.isEmpty(newRecord.get("ClassId"))) + { + Long classId = Long.valueOf(newRecord.get("ClassId")); + dataTypeRowCounts.merge(classId, 1L, Long::sum); + } + } + }); + if (!lsids.isEmpty()) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("LSID"), lsids, CompareType.IN); + TableSelector selector = new TableSelector(ExperimentService.get().getTinfoData(), Collections.singleton("RowId"), filter, null); + sourceIds.addAll(selector.getArrayList(Long.class)); + } + return Pair.of(sourceIds, dataTypeRowCounts); + } +} diff --git a/experiment/src/org/labkey/experiment/ExpDataIterators.java b/experiment/src/org/labkey/experiment/ExpDataIterators.java index 108fdb61148..faa50bd4fa2 100644 --- a/experiment/src/org/labkey/experiment/ExpDataIterators.java +++ b/experiment/src/org/labkey/experiment/ExpDataIterators.java @@ -96,6 +96,7 @@ import org.labkey.api.exp.query.SamplesSchema; import org.labkey.api.qc.DataState; import org.labkey.api.qc.SampleStatusService; +import org.labkey.api.query.AbstractQueryImportAction; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.FieldKey; import org.labkey.api.query.FileColumnValueMapper; @@ -2379,7 +2380,10 @@ public DataIterator getDataIterator(DataIteratorContext context) // useTransactionAuditCache already set for import and merge in AbstractQueryImportAction.createDataIteratorContext if (context.getInsertOption() == QueryUpdateService.InsertOption.INSERT) - context.setUseTransactionAuditCache(true); + { + if (context.getConfigParameters().isEmpty() || context.getConfigParameterBoolean(AbstractQueryImportAction.Params.useTransactionAuditCache)) + context.setUseTransactionAuditCache(true); + } // add FileLink DataIterator if any input columns are of type FILE_LINK if (null != _fileLinkDirectory) From 100427b241f31fa6aca2b2f79c573120002a83c9 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 6 May 2026 20:24:50 -0700 Subject: [PATCH 2/2] crlf --- .../src/org/labkey/audit/AuditController.java | 1172 ++++++++--------- audit/src/org/labkey/audit/AuditLogImpl.java | 630 ++++----- 2 files changed, 901 insertions(+), 901 deletions(-) diff --git a/audit/src/org/labkey/audit/AuditController.java b/audit/src/org/labkey/audit/AuditController.java index 9c420cec5f8..9778cd7dbbb 100644 --- a/audit/src/org/labkey/audit/AuditController.java +++ b/audit/src/org/labkey/audit/AuditController.java @@ -1,586 +1,586 @@ -/* - * 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.audit; - -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.action.ApiSimpleResponse; -import org.labkey.api.action.MutatingApiAction; -import org.labkey.api.action.QueryViewAction; -import org.labkey.api.action.ReadOnlyApiAction; -import org.labkey.api.action.SimpleRedirectAction; -import org.labkey.api.action.SimpleViewAction; -import org.labkey.api.action.SpringActionController; -import org.labkey.api.admin.AdminUrls; -import org.labkey.api.audit.AbstractAuditTypeProvider; -import org.labkey.api.audit.AuditLogService; -import org.labkey.api.audit.AuditUrls; -import org.labkey.api.audit.DetailedAuditTypeEvent; -import org.labkey.api.audit.permissions.CanSeeAuditLogPermission; -import org.labkey.api.audit.provider.SiteSettingsAuditProvider; -import org.labkey.api.audit.view.AuditChangesView; -import org.labkey.api.data.CompareType; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerFilter; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.SimpleFilter; -import org.labkey.api.query.FieldKey; -import org.labkey.api.query.QuerySettings; -import org.labkey.api.query.QueryUrls; -import org.labkey.api.query.QueryView; -import org.labkey.api.query.UserSchema; -import org.labkey.api.security.ElevatedUser; -import org.labkey.api.security.RequiresPermission; -import org.labkey.api.security.User; -import org.labkey.api.security.permissions.AdminPermission; -import org.labkey.api.security.permissions.ReadPermission; -import org.labkey.api.security.permissions.TroubleshooterPermission; -import org.labkey.api.security.roles.ReaderRole; -import org.labkey.api.settings.AdminConsole; -import org.labkey.api.settings.LookAndFeelProperties; -import org.labkey.api.util.DateUtil; -import org.labkey.api.util.Pair; -import org.labkey.api.view.ActionURL; -import org.labkey.api.view.HttpView; -import org.labkey.api.view.JspView; -import org.labkey.api.view.NavTree; -import org.labkey.api.view.NotFoundException; -import org.labkey.api.view.VBox; -import org.springframework.validation.BindException; -import org.springframework.validation.Errors; -import org.springframework.web.servlet.ModelAndView; - -import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.Map; - -import static org.labkey.api.data.ContainerManager.REQUIRE_USER_COMMENTS_PROPERTY_NAME; - -public class AuditController extends SpringActionController -{ - private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(AuditController.class); - - public AuditController() - { - setActionResolver(_actionResolver); - } - - public static void registerAdminConsoleLinks() - { - AdminConsole.addLink(AdminConsole.SettingsLinkType.Management, "audit log", new ActionURL(ShowAuditLogAction.class, ContainerManager.getRoot()), CanSeeAuditLogPermission.class); - } - - public static class AuditUrlsImpl implements AuditUrls - { - @Override - public ActionURL getAuditLog(Container container, String eventType, @Nullable Date startDate, @Nullable Date endDate) - { - ActionURL url = new ActionURL(AuditLogAction.class, container).addParameter("eventType", eventType); - - if (startDate != null) - url.addParameter("startDate", DateUtil.toISO(startDate)); - if (endDate != null) - url.addParameter("endDate", DateUtil.toISO(endDate)); - - return url; - } - } - - @RequiresPermission(AdminPermission.class) - public class BeginAction extends SimpleRedirectAction - { - @Override - public ActionURL getRedirectURL(Object o) - { - if (getContainer() != null && getContainer().isRoot()) - return new ActionURL(ShowAuditLogAction.class, getContainer()); - else - return urlProvider(QueryUrls.class).urlSchemaBrowser(getContainer(), "auditLog"); - } - } - - // An admin console action, but we want Troubleshooters to be able to POST (for export) - @RequiresPermission(TroubleshooterPermission.class) - public static class ShowAuditLogAction extends QueryViewAction - { - public ShowAuditLogAction() - { - super(ShowAuditLogForm.class); - } - - @Override - protected ModelAndView getHtmlView(ShowAuditLogForm form, BindException errors) throws Exception - { - VBox view = new VBox(); - - JspView jspView = new JspView<>("/org/labkey/audit/auditLog.jsp", form.getView()); - - view.addView(jspView); - view.addView(createInitializedQueryView(form, errors, false, null)); - - return view; - } - - @Override - protected QueryView createQueryView(ShowAuditLogForm form, BindException errors, boolean forExport, String dataRegion) - { - // Troubleshooters don't have read permission, so add Reader as a contextual role to placate DataRegion's - // and ButtonBar's render-time permissions check. See #39638 - if (!getContainer().hasPermission(getUser(), ReadPermission.class)) - getViewContext().addContextualRole(ReaderRole.class); - - String selected = form.getView(); - - if (selected == null) - selected = AuditLogService.get().getAuditProviders().get(0).getEventName(); - - UserSchema schema = AuditLogService.getAuditLogSchema(getUser(), getContainer()); - QuerySettings settings = new QuerySettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, selected); - settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); - - return schema.createView(getViewContext(), settings, errors); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("audits"); - urlProvider(AdminUrls.class).addAdminNavTrail(root, "Audit Log", getClass(), getContainer()); - } - } - - public static class ShowAuditLogForm extends QueryViewAction.QueryExportForm - { - private String _view; - - public String getView() - { - return _view; - } - - @SuppressWarnings({"UnusedDeclaration"}) - public void setView(String view) - { - _view = view; - } - } - - - public static class SiteSettingsAuditDetailsForm - { - private Integer _id; - - public Integer getId() - { - return _id; - } - - public void setId(Integer id) - { - _id = id; - } - } - - @RequiresPermission(AdminPermission.class) - public class ShowSiteSettingsAuditDetailsAction extends SimpleViewAction - { - @Override - public ModelAndView getView(SiteSettingsAuditDetailsForm form, BindException errors) - { - if (null == form.getId() || form.getId().intValue() < 0) - throw new NotFoundException("The audit log details key was not provided!"); - - String diff = null; - User createdBy = null; - Date created = null; - - SiteSettingsAuditProvider.SiteSettingsAuditEvent event = AuditLogService.get().getAuditEvent(getUser(), SiteSettingsAuditProvider.AUDIT_EVENT_TYPE, form.getId()); - - if (event != null) - { - diff = event.getChanges(); - createdBy = event.getCreatedBy(); - created = event.getCreated(); - } - - SiteSettingsAuditDetailsModel model = new SiteSettingsAuditDetailsModel(getContainer(), diff, createdBy, created); - return new JspView<>("/org/labkey/audit/siteSettingsAuditDetails.jsp", model); - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Admin Console", urlProvider(AdminUrls.class).getAdminConsoleURL()); - - ActionURL urlLog = new ActionURL(ShowAuditLogAction.class, ContainerManager.getRoot()); - urlLog.addParameter("view", SiteSettingsAuditProvider.AUDIT_EVENT_TYPE); - root.addChild("Audit Log", urlLog); - root.addChild("Site Settings Audit Event Details"); - } - } - - @SuppressWarnings({"unused"}) - @RequiresPermission(ReadPermission.class) - public static class GetDetailedAuditChangesAction extends ReadOnlyApiAction - { - private @NotNull ContainerFilter getContainerFilter(AuditChangesForm form) throws IllegalArgumentException - { - Container container = getContainer(); - User user = getUser(); - - if (!StringUtils.isEmpty(form.getContainerFilter())) - return ContainerFilter.Type.valueOf(form.getContainerFilter()).create(container, user); - - return ContainerFilter.Type.Current.create(container, user); - } - - @Override - public Object execute(AuditChangesForm form, BindException errors) - { - ApiSimpleResponse response = new ApiSimpleResponse(); - DetailedAuditTypeEvent event = AuditLogService.get().getAuditEvent(getUser(), form.getAuditEventType(), form.getAuditRowId(), getContainerFilter(form)); - - if (event != null) - { - response.put("comment", event.getComment()); - response.put("eventUserId", event.getCreatedBy() != null ? event.getCreatedBy().getUserId() : null); - response.put("eventDateFormatted", new SimpleDateFormat(LookAndFeelProperties.getInstance(getContainer()).getDefaultDateTimeFormat()).format(event.getCreated())); - if (event.getUserComment() != null) - response.put("userComment", event.getUserComment()); - - String oldRecord = event.getOldRecordMap(); - String newRecord = event.getNewRecordMap(); - - if (oldRecord != null || newRecord != null) - { - response.put("oldData", AbstractAuditTypeProvider.decodeFromDataMap(oldRecord)); - response.put("newData", AbstractAuditTypeProvider.decodeFromDataMap(newRecord)); - } - - response.put("success", true); - return response; - } - - response.put("success", false); - return response; - } - } - - @SuppressWarnings({"unused"}) - @RequiresPermission(ReadPermission.class) - public static class DetailedAuditChangesAction extends SimpleViewAction - { - @Override - public ModelAndView getView(AuditChangesForm form, BindException errors) - { - int auditRowId = form.getAuditRowId(); - String comment = null; - String oldRecord = null; - String newRecord = null; - - DetailedAuditTypeEvent event = AuditLogService.get().getAuditEvent(getUser(), form.getAuditEventType(), auditRowId); - - if (event != null) - { - comment = event.getComment(); - oldRecord = event.getOldRecordMap(); - newRecord = event.getNewRecordMap(); - } - - if (oldRecord != null || newRecord != null) - { - Map oldData = AbstractAuditTypeProvider.decodeFromDataMap(oldRecord); - Map newData = AbstractAuditTypeProvider.decodeFromDataMap(newRecord); - - return new AuditChangesView(comment, oldData, newData); - } - return new NoRecordView(); - } - - private static class NoRecordView extends HttpView - { - @Override - protected void renderInternal(Object model, PrintWriter out) - { - out.write("

No current record found

"); - } - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Audit Details"); - } - } - - @SuppressWarnings({"unused", "WeakerAccess"}) - public static class AuditChangesForm - { - private int auditRowId; - private String auditEventType; - private String _containerFilter; - - public int getAuditRowId() - { - return auditRowId; - } - - public void setAuditRowId(int auditRowId) - { - this.auditRowId = auditRowId; - } - - public String getAuditEventType() - { - return auditEventType; - } - - public void setAuditEventType(String auditEventType) - { - this.auditEventType = auditEventType; - } - - public String getContainerFilter() - { - return _containerFilter; - } - - public void setContainerFilter(String containerFilter) - { - _containerFilter = containerFilter; - } - - } - - @RequiresPermission(ReadPermission.class) - public static class GetTransactionRowIdsAction extends ReadOnlyApiAction - { - @Override - public void validateForm(AuditTransactionForm form, Errors errors) - { - form.validate(errors); - } - - @Override - public Object execute(AuditTransactionForm form, BindException errors) - { - Pair, Map> results; - User elevatedUser = ElevatedUser.ensureCanSeeAuditLogRole(getContainer(), getUser()); - ContainerFilter cf = ContainerFilter.getContainerFilterByName(form.getContainerFilter(), getContainer(), elevatedUser); - if (form.isSampleType()) - results = AuditLogImpl.get().getTransactionSampleIds(form.getTransactionAuditId(), elevatedUser, getContainer(), cf); - else - results = AuditLogImpl.get().getTransactionSourceIds(form.getTransactionAuditId(), elevatedUser, getContainer(), cf); - - ApiSimpleResponse response = new ApiSimpleResponse(); - response.put("success", true); - response.put("rowIds", results.first); - response.put("dataTypeIds", results.second); - - return response; - } - } - - public static class AuditTransactionForm - { - private Long _transactionAuditId; - private String _dataType; - private boolean _isSampleType; - String _containerFilter; - - public Long getTransactionAuditId() - { - return _transactionAuditId; - } - - public void setTransactionAuditId(Long transactionAuditId) - { - _transactionAuditId = transactionAuditId; - } - - public String getDataType() - { - return _dataType; - } - - public void setDataType(String dataType) - { - _dataType = dataType; - } - - public boolean isSampleType() - { - return _isSampleType; - } - - public String getContainerFilter() - { - return _containerFilter; - } - - public void setContainerFilter(String containerFilter) - { - _containerFilter = containerFilter; - } - - public void validate(Errors errors) - { - if (getTransactionAuditId() == null) - errors.reject(ERROR_REQUIRED, "'transactionAuditId' is required"); - if (getDataType() == null) - errors.reject(ERROR_REQUIRED, "'dataType' is required"); - else - { - _isSampleType = getDataType().equalsIgnoreCase("samples"); - if (!_isSampleType && !getDataType().equalsIgnoreCase("sources")) - errors.reject(ERROR_MSG, "Unknown dataType: " + getDataType()); - } - } - } - - public static class AuditLogForm - { - private String _eventType; - private Date _startDate; - private Date _endDate; - - public String getEventType() - { - return _eventType; - } - - public void setEventType(String eventType) - { - _eventType = eventType; - } - - public Date getStartDate() - { - return _startDate; - } - - public void setStartDate(Date startDate) - { - _startDate = startDate; - } - - public Date getEndDate() - { - return _endDate; - } - - public void setEndDate(Date endDate) - { - _endDate = endDate; - } - } - - @RequiresPermission(CanSeeAuditLogPermission.class) - public static class AuditLogAction extends SimpleViewAction - { - private String _eventType; - - @Override - public ModelAndView getView(AuditLogForm form, BindException errors) throws Exception - { - _eventType = form.getEventType(); - - UserSchema schema = AuditLogService.getAuditLogSchema(getUser(), getContainer()); - QuerySettings settings = new QuerySettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, _eventType); - - SimpleFilter filter = new SimpleFilter(); - if (form.getStartDate() != null) - { - Calendar c = new GregorianCalendar(); - c.setTime(form.getStartDate()); - filter.addCondition(FieldKey.fromParts("created"), c, CompareType.DATE_GTE); - } - - if (form.getEndDate() != null) - { - Calendar c = new GregorianCalendar(); - c.setTime(form.getEndDate()); - filter.addCondition(FieldKey.fromParts("created"), c, CompareType.DATE_LTE); - } - - // add additional filters that may be on the URL - filter.addUrlFilters(getViewContext().getActionURL(), QueryView.DATAREGIONNAME_DEFAULT); - settings.setBaseFilter(filter); - - return schema.createView(getViewContext(), settings, errors); - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild(_eventType + " : Audit Log"); - } - } - - @RequiresPermission(AdminPermission.class) - public static class SaveAuditSettingsAction extends MutatingApiAction - { - @Override - public void validateForm(AuditSettingsForm form, Errors errors) - { - if (!getContainer().isAppHomeFolder()) - errors.reject(ERROR_GENERIC, "This action is not supported for sub-folders of the application."); - if (form.getRequireUserComments() == null) - errors.reject(ERROR_REQUIRED, "requireUserComments is required to be non-null."); - } - - @Override - public Object execute(AuditSettingsForm form, BindException errors) - { - ContainerManager.setRequireAuditComments(getContainer(), getUser(), form.getRequireUserComments()); - return success(); - } - } - - public static class AuditSettingsForm - { - private Boolean _requireUserComments; - - public Boolean getRequireUserComments() - { - return _requireUserComments; - } - - public void setRequireUserComments(Boolean requireUserComments) - { - _requireUserComments = requireUserComments; - } - } - - @RequiresPermission(ReadPermission.class) - public static class GetAuditSettingsAction extends ReadOnlyApiAction - { - @Override - public Object execute(Object o, BindException errors) - { - Container container = getContainer(); - if (!container.isAppHomeFolder()) - container = container.getProject(); - return container == null ? Collections.emptyMap() : Map.of(REQUIRE_USER_COMMENTS_PROPERTY_NAME, container.getAuditCommentsRequired()); - } - } -} +/* + * 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.audit; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.action.ApiSimpleResponse; +import org.labkey.api.action.MutatingApiAction; +import org.labkey.api.action.QueryViewAction; +import org.labkey.api.action.ReadOnlyApiAction; +import org.labkey.api.action.SimpleRedirectAction; +import org.labkey.api.action.SimpleViewAction; +import org.labkey.api.action.SpringActionController; +import org.labkey.api.admin.AdminUrls; +import org.labkey.api.audit.AbstractAuditTypeProvider; +import org.labkey.api.audit.AuditLogService; +import org.labkey.api.audit.AuditUrls; +import org.labkey.api.audit.DetailedAuditTypeEvent; +import org.labkey.api.audit.permissions.CanSeeAuditLogPermission; +import org.labkey.api.audit.provider.SiteSettingsAuditProvider; +import org.labkey.api.audit.view.AuditChangesView; +import org.labkey.api.data.CompareType; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.QuerySettings; +import org.labkey.api.query.QueryUrls; +import org.labkey.api.query.QueryView; +import org.labkey.api.query.UserSchema; +import org.labkey.api.security.ElevatedUser; +import org.labkey.api.security.RequiresPermission; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.security.permissions.TroubleshooterPermission; +import org.labkey.api.security.roles.ReaderRole; +import org.labkey.api.settings.AdminConsole; +import org.labkey.api.settings.LookAndFeelProperties; +import org.labkey.api.util.DateUtil; +import org.labkey.api.util.Pair; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.HttpView; +import org.labkey.api.view.JspView; +import org.labkey.api.view.NavTree; +import org.labkey.api.view.NotFoundException; +import org.labkey.api.view.VBox; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.servlet.ModelAndView; + +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Map; + +import static org.labkey.api.data.ContainerManager.REQUIRE_USER_COMMENTS_PROPERTY_NAME; + +public class AuditController extends SpringActionController +{ + private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(AuditController.class); + + public AuditController() + { + setActionResolver(_actionResolver); + } + + public static void registerAdminConsoleLinks() + { + AdminConsole.addLink(AdminConsole.SettingsLinkType.Management, "audit log", new ActionURL(ShowAuditLogAction.class, ContainerManager.getRoot()), CanSeeAuditLogPermission.class); + } + + public static class AuditUrlsImpl implements AuditUrls + { + @Override + public ActionURL getAuditLog(Container container, String eventType, @Nullable Date startDate, @Nullable Date endDate) + { + ActionURL url = new ActionURL(AuditLogAction.class, container).addParameter("eventType", eventType); + + if (startDate != null) + url.addParameter("startDate", DateUtil.toISO(startDate)); + if (endDate != null) + url.addParameter("endDate", DateUtil.toISO(endDate)); + + return url; + } + } + + @RequiresPermission(AdminPermission.class) + public class BeginAction extends SimpleRedirectAction + { + @Override + public ActionURL getRedirectURL(Object o) + { + if (getContainer() != null && getContainer().isRoot()) + return new ActionURL(ShowAuditLogAction.class, getContainer()); + else + return urlProvider(QueryUrls.class).urlSchemaBrowser(getContainer(), "auditLog"); + } + } + + // An admin console action, but we want Troubleshooters to be able to POST (for export) + @RequiresPermission(TroubleshooterPermission.class) + public static class ShowAuditLogAction extends QueryViewAction + { + public ShowAuditLogAction() + { + super(ShowAuditLogForm.class); + } + + @Override + protected ModelAndView getHtmlView(ShowAuditLogForm form, BindException errors) throws Exception + { + VBox view = new VBox(); + + JspView jspView = new JspView<>("/org/labkey/audit/auditLog.jsp", form.getView()); + + view.addView(jspView); + view.addView(createInitializedQueryView(form, errors, false, null)); + + return view; + } + + @Override + protected QueryView createQueryView(ShowAuditLogForm form, BindException errors, boolean forExport, String dataRegion) + { + // Troubleshooters don't have read permission, so add Reader as a contextual role to placate DataRegion's + // and ButtonBar's render-time permissions check. See #39638 + if (!getContainer().hasPermission(getUser(), ReadPermission.class)) + getViewContext().addContextualRole(ReaderRole.class); + + String selected = form.getView(); + + if (selected == null) + selected = AuditLogService.get().getAuditProviders().get(0).getEventName(); + + UserSchema schema = AuditLogService.getAuditLogSchema(getUser(), getContainer()); + QuerySettings settings = new QuerySettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, selected); + settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); + + return schema.createView(getViewContext(), settings, errors); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("audits"); + urlProvider(AdminUrls.class).addAdminNavTrail(root, "Audit Log", getClass(), getContainer()); + } + } + + public static class ShowAuditLogForm extends QueryViewAction.QueryExportForm + { + private String _view; + + public String getView() + { + return _view; + } + + @SuppressWarnings({"UnusedDeclaration"}) + public void setView(String view) + { + _view = view; + } + } + + + public static class SiteSettingsAuditDetailsForm + { + private Integer _id; + + public Integer getId() + { + return _id; + } + + public void setId(Integer id) + { + _id = id; + } + } + + @RequiresPermission(AdminPermission.class) + public class ShowSiteSettingsAuditDetailsAction extends SimpleViewAction + { + @Override + public ModelAndView getView(SiteSettingsAuditDetailsForm form, BindException errors) + { + if (null == form.getId() || form.getId().intValue() < 0) + throw new NotFoundException("The audit log details key was not provided!"); + + String diff = null; + User createdBy = null; + Date created = null; + + SiteSettingsAuditProvider.SiteSettingsAuditEvent event = AuditLogService.get().getAuditEvent(getUser(), SiteSettingsAuditProvider.AUDIT_EVENT_TYPE, form.getId()); + + if (event != null) + { + diff = event.getChanges(); + createdBy = event.getCreatedBy(); + created = event.getCreated(); + } + + SiteSettingsAuditDetailsModel model = new SiteSettingsAuditDetailsModel(getContainer(), diff, createdBy, created); + return new JspView<>("/org/labkey/audit/siteSettingsAuditDetails.jsp", model); + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Admin Console", urlProvider(AdminUrls.class).getAdminConsoleURL()); + + ActionURL urlLog = new ActionURL(ShowAuditLogAction.class, ContainerManager.getRoot()); + urlLog.addParameter("view", SiteSettingsAuditProvider.AUDIT_EVENT_TYPE); + root.addChild("Audit Log", urlLog); + root.addChild("Site Settings Audit Event Details"); + } + } + + @SuppressWarnings({"unused"}) + @RequiresPermission(ReadPermission.class) + public static class GetDetailedAuditChangesAction extends ReadOnlyApiAction + { + private @NotNull ContainerFilter getContainerFilter(AuditChangesForm form) throws IllegalArgumentException + { + Container container = getContainer(); + User user = getUser(); + + if (!StringUtils.isEmpty(form.getContainerFilter())) + return ContainerFilter.Type.valueOf(form.getContainerFilter()).create(container, user); + + return ContainerFilter.Type.Current.create(container, user); + } + + @Override + public Object execute(AuditChangesForm form, BindException errors) + { + ApiSimpleResponse response = new ApiSimpleResponse(); + DetailedAuditTypeEvent event = AuditLogService.get().getAuditEvent(getUser(), form.getAuditEventType(), form.getAuditRowId(), getContainerFilter(form)); + + if (event != null) + { + response.put("comment", event.getComment()); + response.put("eventUserId", event.getCreatedBy() != null ? event.getCreatedBy().getUserId() : null); + response.put("eventDateFormatted", new SimpleDateFormat(LookAndFeelProperties.getInstance(getContainer()).getDefaultDateTimeFormat()).format(event.getCreated())); + if (event.getUserComment() != null) + response.put("userComment", event.getUserComment()); + + String oldRecord = event.getOldRecordMap(); + String newRecord = event.getNewRecordMap(); + + if (oldRecord != null || newRecord != null) + { + response.put("oldData", AbstractAuditTypeProvider.decodeFromDataMap(oldRecord)); + response.put("newData", AbstractAuditTypeProvider.decodeFromDataMap(newRecord)); + } + + response.put("success", true); + return response; + } + + response.put("success", false); + return response; + } + } + + @SuppressWarnings({"unused"}) + @RequiresPermission(ReadPermission.class) + public static class DetailedAuditChangesAction extends SimpleViewAction + { + @Override + public ModelAndView getView(AuditChangesForm form, BindException errors) + { + int auditRowId = form.getAuditRowId(); + String comment = null; + String oldRecord = null; + String newRecord = null; + + DetailedAuditTypeEvent event = AuditLogService.get().getAuditEvent(getUser(), form.getAuditEventType(), auditRowId); + + if (event != null) + { + comment = event.getComment(); + oldRecord = event.getOldRecordMap(); + newRecord = event.getNewRecordMap(); + } + + if (oldRecord != null || newRecord != null) + { + Map oldData = AbstractAuditTypeProvider.decodeFromDataMap(oldRecord); + Map newData = AbstractAuditTypeProvider.decodeFromDataMap(newRecord); + + return new AuditChangesView(comment, oldData, newData); + } + return new NoRecordView(); + } + + private static class NoRecordView extends HttpView + { + @Override + protected void renderInternal(Object model, PrintWriter out) + { + out.write("

No current record found

"); + } + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Audit Details"); + } + } + + @SuppressWarnings({"unused", "WeakerAccess"}) + public static class AuditChangesForm + { + private int auditRowId; + private String auditEventType; + private String _containerFilter; + + public int getAuditRowId() + { + return auditRowId; + } + + public void setAuditRowId(int auditRowId) + { + this.auditRowId = auditRowId; + } + + public String getAuditEventType() + { + return auditEventType; + } + + public void setAuditEventType(String auditEventType) + { + this.auditEventType = auditEventType; + } + + public String getContainerFilter() + { + return _containerFilter; + } + + public void setContainerFilter(String containerFilter) + { + _containerFilter = containerFilter; + } + + } + + @RequiresPermission(ReadPermission.class) + public static class GetTransactionRowIdsAction extends ReadOnlyApiAction + { + @Override + public void validateForm(AuditTransactionForm form, Errors errors) + { + form.validate(errors); + } + + @Override + public Object execute(AuditTransactionForm form, BindException errors) + { + Pair, Map> results; + User elevatedUser = ElevatedUser.ensureCanSeeAuditLogRole(getContainer(), getUser()); + ContainerFilter cf = ContainerFilter.getContainerFilterByName(form.getContainerFilter(), getContainer(), elevatedUser); + if (form.isSampleType()) + results = AuditLogImpl.get().getTransactionSampleIds(form.getTransactionAuditId(), elevatedUser, getContainer(), cf); + else + results = AuditLogImpl.get().getTransactionSourceIds(form.getTransactionAuditId(), elevatedUser, getContainer(), cf); + + ApiSimpleResponse response = new ApiSimpleResponse(); + response.put("success", true); + response.put("rowIds", results.first); + response.put("dataTypeIds", results.second); + + return response; + } + } + + public static class AuditTransactionForm + { + private Long _transactionAuditId; + private String _dataType; + private boolean _isSampleType; + String _containerFilter; + + public Long getTransactionAuditId() + { + return _transactionAuditId; + } + + public void setTransactionAuditId(Long transactionAuditId) + { + _transactionAuditId = transactionAuditId; + } + + public String getDataType() + { + return _dataType; + } + + public void setDataType(String dataType) + { + _dataType = dataType; + } + + public boolean isSampleType() + { + return _isSampleType; + } + + public String getContainerFilter() + { + return _containerFilter; + } + + public void setContainerFilter(String containerFilter) + { + _containerFilter = containerFilter; + } + + public void validate(Errors errors) + { + if (getTransactionAuditId() == null) + errors.reject(ERROR_REQUIRED, "'transactionAuditId' is required"); + if (getDataType() == null) + errors.reject(ERROR_REQUIRED, "'dataType' is required"); + else + { + _isSampleType = getDataType().equalsIgnoreCase("samples"); + if (!_isSampleType && !getDataType().equalsIgnoreCase("sources")) + errors.reject(ERROR_MSG, "Unknown dataType: " + getDataType()); + } + } + } + + public static class AuditLogForm + { + private String _eventType; + private Date _startDate; + private Date _endDate; + + public String getEventType() + { + return _eventType; + } + + public void setEventType(String eventType) + { + _eventType = eventType; + } + + public Date getStartDate() + { + return _startDate; + } + + public void setStartDate(Date startDate) + { + _startDate = startDate; + } + + public Date getEndDate() + { + return _endDate; + } + + public void setEndDate(Date endDate) + { + _endDate = endDate; + } + } + + @RequiresPermission(CanSeeAuditLogPermission.class) + public static class AuditLogAction extends SimpleViewAction + { + private String _eventType; + + @Override + public ModelAndView getView(AuditLogForm form, BindException errors) throws Exception + { + _eventType = form.getEventType(); + + UserSchema schema = AuditLogService.getAuditLogSchema(getUser(), getContainer()); + QuerySettings settings = new QuerySettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, _eventType); + + SimpleFilter filter = new SimpleFilter(); + if (form.getStartDate() != null) + { + Calendar c = new GregorianCalendar(); + c.setTime(form.getStartDate()); + filter.addCondition(FieldKey.fromParts("created"), c, CompareType.DATE_GTE); + } + + if (form.getEndDate() != null) + { + Calendar c = new GregorianCalendar(); + c.setTime(form.getEndDate()); + filter.addCondition(FieldKey.fromParts("created"), c, CompareType.DATE_LTE); + } + + // add additional filters that may be on the URL + filter.addUrlFilters(getViewContext().getActionURL(), QueryView.DATAREGIONNAME_DEFAULT); + settings.setBaseFilter(filter); + + return schema.createView(getViewContext(), settings, errors); + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild(_eventType + " : Audit Log"); + } + } + + @RequiresPermission(AdminPermission.class) + public static class SaveAuditSettingsAction extends MutatingApiAction + { + @Override + public void validateForm(AuditSettingsForm form, Errors errors) + { + if (!getContainer().isAppHomeFolder()) + errors.reject(ERROR_GENERIC, "This action is not supported for sub-folders of the application."); + if (form.getRequireUserComments() == null) + errors.reject(ERROR_REQUIRED, "requireUserComments is required to be non-null."); + } + + @Override + public Object execute(AuditSettingsForm form, BindException errors) + { + ContainerManager.setRequireAuditComments(getContainer(), getUser(), form.getRequireUserComments()); + return success(); + } + } + + public static class AuditSettingsForm + { + private Boolean _requireUserComments; + + public Boolean getRequireUserComments() + { + return _requireUserComments; + } + + public void setRequireUserComments(Boolean requireUserComments) + { + _requireUserComments = requireUserComments; + } + } + + @RequiresPermission(ReadPermission.class) + public static class GetAuditSettingsAction extends ReadOnlyApiAction + { + @Override + public Object execute(Object o, BindException errors) + { + Container container = getContainer(); + if (!container.isAppHomeFolder()) + container = container.getProject(); + return container == null ? Collections.emptyMap() : Map.of(REQUIRE_USER_COMMENTS_PROPERTY_NAME, container.getAuditCommentsRequired()); + } + } +} diff --git a/audit/src/org/labkey/audit/AuditLogImpl.java b/audit/src/org/labkey/audit/AuditLogImpl.java index 3c0b6479b6e..ce488afbde7 100644 --- a/audit/src/org/labkey/audit/AuditLogImpl.java +++ b/audit/src/org/labkey/audit/AuditLogImpl.java @@ -1,315 +1,315 @@ -/* - * 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.audit; - -import jakarta.servlet.ServletContext; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.action.SpringActionController; -import org.labkey.api.audit.AbstractAuditTypeProvider; -import org.labkey.api.audit.AuditLogService; -import org.labkey.api.audit.AuditTypeEvent; -import org.labkey.api.audit.AuditTypeProvider; -import org.labkey.api.audit.DetailedAuditTypeEvent; -import org.labkey.api.audit.SampleTimelineAuditEvent; -import org.labkey.api.cache.Cache; -import org.labkey.api.cache.CacheManager; -import org.labkey.api.collections.CaseInsensitiveHashMap; -import org.labkey.api.data.CompareType; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerFilter; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.SimpleFilter; -import org.labkey.api.data.Sort; -import org.labkey.api.data.TableSelector; -import org.labkey.api.exp.api.ExperimentService; -import org.labkey.api.module.ModuleLoader; -import org.labkey.api.query.FieldKey; -import org.labkey.api.query.QueryService; -import org.labkey.api.query.UserSchema; -import org.labkey.api.security.User; -import org.labkey.api.security.UserManager; -import org.labkey.api.util.ContextListener; -import org.labkey.api.util.Pair; -import org.labkey.api.util.StartupListener; -import org.labkey.api.util.logging.LogHelper; -import org.labkey.api.view.ActionURL; -import org.labkey.api.view.HttpView; -import org.labkey.audit.model.LogManager; -import org.labkey.audit.query.AuditQuerySchema; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - -public class AuditLogImpl implements AuditLogService, StartupListener -{ - private static final AuditLogImpl _instance = new AuditLogImpl(); - - private static final Logger _log = LogHelper.getLogger(AuditLogImpl.class, "Audit service interactions."); - - private final Queue> _eventTypeQueue = new LinkedList<>(); - private final AtomicBoolean _logToDatabase = new AtomicBoolean(false); - private static final Object STARTUP_LOCK = new Object(); - - // Cache the audit events associated with transaction ids. We currently use these for interacting with objects - // that were created immediately after they were created, so the cache size does not need to be very large and the defaultTimeToLive can be small. - // Use a pair as the cache object to avoid warnings about mutable cache objects (Issue 48779). - // Since this is all about capturing data from the same transaction, there shouldn't be other threads in the mix. - private static final Cache>> TRANSACTION_EVENT_CACHE = CacheManager.getBlockingCache(50, CacheManager.HOUR, - "Transaction Audit Event Cache", - (key, argument) -> Pair.of(key, new ArrayList<>()) - ); - - public static AuditLogImpl get() - { - return _instance; - } - - private AuditLogImpl() - { - // If we're migrating, avoid creating all the audit log tables and inserting the queued events - if (ModuleLoader.getInstance().shouldInsertData()) - ContextListener.addStartupListener(this); - } - - @Override - public String getName() - { - return "Audit Log"; - } - - @Override - public void moduleStartupComplete(ServletContext servletContext) - { - // perform audit provider initialization - for (AuditTypeProvider provider : AuditLogService.get().getAuditProviders()) - { - provider.initializeProvider(User.getAdminServiceUser()); - } - - // Synchronize so that we can guarantee that all events have already been added to the queue before we - // start processing them - synchronized (STARTUP_LOCK) - { - _logToDatabase.set(true); - } - - while (!_eventTypeQueue.isEmpty()) - { - Pair event = _eventTypeQueue.remove(); - addEvents(event.first, List.of(event.second)); - } - } - - @Override - public boolean isViewable() - { - return true; - } - - @Override - public K addEvent(User user, K event) - { - return _addEvents(user, List.of(event),true, false); - } - - @Override - public void addEvents(@Nullable User user, List events) - { - _addEvents(user, events, false, false); - } - - @Override - public void addEvents(@Nullable User user, List events, boolean useTransactionAuditCache) - { - _addEvents(user, events, false, useTransactionAuditCache); - } - - private K _addEvents(@Nullable User user, List events, boolean reselectEvent, boolean useTransactionAuditCache) - { - assert !reselectEvent || events.size() == 1; - - for (var event : events) - { - assert event.getContainer() != null : "Container cannot be null"; - - if (user == null) - { - if (HttpView.hasCurrentView() && HttpView.currentContext() != null) - _log.warn("user was not specified for event type " + event.getEventType() + " in container " + event.getContainer() + "; defaulting to guest user."); - user = UserManager.getGuestUser(); - } - if (event.getTransactionId() != null && useTransactionAuditCache) - { - List transactionEvents = TRANSACTION_EVENT_CACHE.get(event.getTransactionId()).second; - transactionEvents.add(event); - } - - if (event.getImpersonatedBy() == null && user.isImpersonated()) - { - User impersonatingUser = user.getImpersonatingUser(); - event.setImpersonatedBy(impersonatingUser.getUserId()); - } - } - - try (var ignored = SpringActionController.ignoreSqlUpdates()) - { - /* - This is necessary because audit log service needs to be registered in the constructor - of the audit module, but the schema may not be created or updated at that point. Events - that occur before startup is complete are therefore queued up and recorded after startup. - */ - boolean databaseReady; - synchronized (STARTUP_LOCK) - { - // Keep the critical section as lean as possible - just guarantee that all the events - // have been queued before releasing the lock - databaseReady = _logToDatabase.get(); - if (!databaseReady) - { - for (var event : events) - _eventTypeQueue.add(new Pair<>(user, event)); - } - } - - if (databaseReady) - { - if (reselectEvent && events.size()==1) - return LogManager.get().insertEvent(user, events.get(0)); - LogManager.get().insertEvents(user, events); - } - } - catch (RuntimeException e) - { - _log.error("Failed to insert audit log event", e); - AuditLogService.handleAuditFailure(user, e); - throw e; - } - return null; - } - - @Override - public UserSchema createSchema(User user, Container container) - { - return new AuditQuerySchema(user, container); - } - - @Nullable - @Override - public K getAuditEvent(User user, String eventType, int rowId) - { - return LogManager.get().getAuditEvent(user, eventType, rowId); - } - - @Nullable - @Override - public K getAuditEvent(User user, String eventType, int rowId, @Nullable ContainerFilter cf) - { - return LogManager.get().getAuditEvent(user, eventType, rowId, cf); - } - - @Override - public List getAuditEvents(Container container, User user, String eventType, @Nullable SimpleFilter filter, @Nullable Sort sort) - { - return LogManager.get().getAuditEvents(container, user, eventType, filter, sort); - } - - @Override - public List getAuditEvents(Container container, User user, String eventType, @Nullable SimpleFilter filter, @Nullable Sort sort, @Nullable ContainerFilter cf) - { - return LogManager.get().getAuditEvents(container, user, eventType, filter, sort, cf); - } - - @Override - public ActionURL getAuditUrl() - { - return new ActionURL(AuditController.ShowAuditLogAction.class, ContainerManager.getRoot()); - } - - public Pair, Map> getTransactionSampleIds(long transactionAuditId, User user, Container container, @Nullable ContainerFilter containerFilter) - { - List transactionEvents = TRANSACTION_EVENT_CACHE.get(transactionAuditId).second; - List events; - if (transactionEvents.isEmpty()) - { - SimpleFilter filter = new SimpleFilter(); - filter.addCondition(FieldKey.fromParts("TransactionID"), transactionAuditId); - events = AuditLogService.get().getAuditEvents(container, user, SampleTimelineAuditEvent.EVENT_TYPE, filter, null, containerFilter); - } - else - { - events = transactionEvents.stream() - .filter(SampleTimelineAuditEvent.class::isInstance) - .map(SampleTimelineAuditEvent.class::cast) - .toList(); - } - Map dataTypeRowCounts = new HashMap<>(); - List sampleIds = new ArrayList<>(); - events.forEach(event -> { - dataTypeRowCounts.merge(event.getSampleTypeId(), 1L, Long::sum); - sampleIds.add(event.getSampleId()); - }); - return Pair.of(sampleIds, dataTypeRowCounts); - } - - public Pair, Map> getTransactionSourceIds(long transactionAuditId, User user, Container container, @Nullable ContainerFilter containerFilter) - { - List lsids = new ArrayList<>(); - List sourceIds = new ArrayList<>(); - Map dataTypeRowCounts = new HashMap<>(); - List transactionEvents = TRANSACTION_EVENT_CACHE.get(transactionAuditId).second; - List detailedEvents = transactionEvents.isEmpty() - ? QueryService.get().getQueryUpdateAuditRecords(user, container, transactionAuditId, containerFilter) - : transactionEvents.stream() - .filter(DetailedAuditTypeEvent.class::isInstance) - .map(DetailedAuditTypeEvent.class::cast) - .toList(); - - detailedEvents.forEach(event -> { - if (event.getNewRecordMap() != null) - { - Map newRecord = new CaseInsensitiveHashMap<>(AbstractAuditTypeProvider.decodeFromDataMap(event.getNewRecordMap())); - if (newRecord.containsKey("RowId") && !StringUtils.isEmpty(newRecord.get("RowId"))) - sourceIds.add(Long.valueOf(newRecord.get("RowId"))); - else if (newRecord.containsKey("LSID") && !StringUtils.isEmpty(newRecord.get("LSID"))) - lsids.add(newRecord.get("LSID")); - - if (newRecord.containsKey("ClassId") && !StringUtils.isEmpty(newRecord.get("ClassId"))) - { - Long classId = Long.valueOf(newRecord.get("ClassId")); - dataTypeRowCounts.merge(classId, 1L, Long::sum); - } - } - }); - if (!lsids.isEmpty()) - { - SimpleFilter filter = SimpleFilter.createContainerFilter(container); - filter.addCondition(FieldKey.fromParts("LSID"), lsids, CompareType.IN); - TableSelector selector = new TableSelector(ExperimentService.get().getTinfoData(), Collections.singleton("RowId"), filter, null); - sourceIds.addAll(selector.getArrayList(Long.class)); - } - return Pair.of(sourceIds, dataTypeRowCounts); - } -} +/* + * 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.audit; + +import jakarta.servlet.ServletContext; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.action.SpringActionController; +import org.labkey.api.audit.AbstractAuditTypeProvider; +import org.labkey.api.audit.AuditLogService; +import org.labkey.api.audit.AuditTypeEvent; +import org.labkey.api.audit.AuditTypeProvider; +import org.labkey.api.audit.DetailedAuditTypeEvent; +import org.labkey.api.audit.SampleTimelineAuditEvent; +import org.labkey.api.cache.Cache; +import org.labkey.api.cache.CacheManager; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.CompareType; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Sort; +import org.labkey.api.data.TableSelector; +import org.labkey.api.exp.api.ExperimentService; +import org.labkey.api.module.ModuleLoader; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.UserSchema; +import org.labkey.api.security.User; +import org.labkey.api.security.UserManager; +import org.labkey.api.util.ContextListener; +import org.labkey.api.util.Pair; +import org.labkey.api.util.StartupListener; +import org.labkey.api.util.logging.LogHelper; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.HttpView; +import org.labkey.audit.model.LogManager; +import org.labkey.audit.query.AuditQuerySchema; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +public class AuditLogImpl implements AuditLogService, StartupListener +{ + private static final AuditLogImpl _instance = new AuditLogImpl(); + + private static final Logger _log = LogHelper.getLogger(AuditLogImpl.class, "Audit service interactions."); + + private final Queue> _eventTypeQueue = new LinkedList<>(); + private final AtomicBoolean _logToDatabase = new AtomicBoolean(false); + private static final Object STARTUP_LOCK = new Object(); + + // Cache the audit events associated with transaction ids. We currently use these for interacting with objects + // that were created immediately after they were created, so the cache size does not need to be very large and the defaultTimeToLive can be small. + // Use a pair as the cache object to avoid warnings about mutable cache objects (Issue 48779). + // Since this is all about capturing data from the same transaction, there shouldn't be other threads in the mix. + private static final Cache>> TRANSACTION_EVENT_CACHE = CacheManager.getBlockingCache(50, CacheManager.HOUR, + "Transaction Audit Event Cache", + (key, argument) -> Pair.of(key, new ArrayList<>()) + ); + + public static AuditLogImpl get() + { + return _instance; + } + + private AuditLogImpl() + { + // If we're migrating, avoid creating all the audit log tables and inserting the queued events + if (ModuleLoader.getInstance().shouldInsertData()) + ContextListener.addStartupListener(this); + } + + @Override + public String getName() + { + return "Audit Log"; + } + + @Override + public void moduleStartupComplete(ServletContext servletContext) + { + // perform audit provider initialization + for (AuditTypeProvider provider : AuditLogService.get().getAuditProviders()) + { + provider.initializeProvider(User.getAdminServiceUser()); + } + + // Synchronize so that we can guarantee that all events have already been added to the queue before we + // start processing them + synchronized (STARTUP_LOCK) + { + _logToDatabase.set(true); + } + + while (!_eventTypeQueue.isEmpty()) + { + Pair event = _eventTypeQueue.remove(); + addEvents(event.first, List.of(event.second)); + } + } + + @Override + public boolean isViewable() + { + return true; + } + + @Override + public K addEvent(User user, K event) + { + return _addEvents(user, List.of(event),true, false); + } + + @Override + public void addEvents(@Nullable User user, List events) + { + _addEvents(user, events, false, false); + } + + @Override + public void addEvents(@Nullable User user, List events, boolean useTransactionAuditCache) + { + _addEvents(user, events, false, useTransactionAuditCache); + } + + private K _addEvents(@Nullable User user, List events, boolean reselectEvent, boolean useTransactionAuditCache) + { + assert !reselectEvent || events.size() == 1; + + for (var event : events) + { + assert event.getContainer() != null : "Container cannot be null"; + + if (user == null) + { + if (HttpView.hasCurrentView() && HttpView.currentContext() != null) + _log.warn("user was not specified for event type " + event.getEventType() + " in container " + event.getContainer() + "; defaulting to guest user."); + user = UserManager.getGuestUser(); + } + if (event.getTransactionId() != null && useTransactionAuditCache) + { + List transactionEvents = TRANSACTION_EVENT_CACHE.get(event.getTransactionId()).second; + transactionEvents.add(event); + } + + if (event.getImpersonatedBy() == null && user.isImpersonated()) + { + User impersonatingUser = user.getImpersonatingUser(); + event.setImpersonatedBy(impersonatingUser.getUserId()); + } + } + + try (var ignored = SpringActionController.ignoreSqlUpdates()) + { + /* + This is necessary because audit log service needs to be registered in the constructor + of the audit module, but the schema may not be created or updated at that point. Events + that occur before startup is complete are therefore queued up and recorded after startup. + */ + boolean databaseReady; + synchronized (STARTUP_LOCK) + { + // Keep the critical section as lean as possible - just guarantee that all the events + // have been queued before releasing the lock + databaseReady = _logToDatabase.get(); + if (!databaseReady) + { + for (var event : events) + _eventTypeQueue.add(new Pair<>(user, event)); + } + } + + if (databaseReady) + { + if (reselectEvent && events.size()==1) + return LogManager.get().insertEvent(user, events.get(0)); + LogManager.get().insertEvents(user, events); + } + } + catch (RuntimeException e) + { + _log.error("Failed to insert audit log event", e); + AuditLogService.handleAuditFailure(user, e); + throw e; + } + return null; + } + + @Override + public UserSchema createSchema(User user, Container container) + { + return new AuditQuerySchema(user, container); + } + + @Nullable + @Override + public K getAuditEvent(User user, String eventType, int rowId) + { + return LogManager.get().getAuditEvent(user, eventType, rowId); + } + + @Nullable + @Override + public K getAuditEvent(User user, String eventType, int rowId, @Nullable ContainerFilter cf) + { + return LogManager.get().getAuditEvent(user, eventType, rowId, cf); + } + + @Override + public List getAuditEvents(Container container, User user, String eventType, @Nullable SimpleFilter filter, @Nullable Sort sort) + { + return LogManager.get().getAuditEvents(container, user, eventType, filter, sort); + } + + @Override + public List getAuditEvents(Container container, User user, String eventType, @Nullable SimpleFilter filter, @Nullable Sort sort, @Nullable ContainerFilter cf) + { + return LogManager.get().getAuditEvents(container, user, eventType, filter, sort, cf); + } + + @Override + public ActionURL getAuditUrl() + { + return new ActionURL(AuditController.ShowAuditLogAction.class, ContainerManager.getRoot()); + } + + public Pair, Map> getTransactionSampleIds(long transactionAuditId, User user, Container container, @Nullable ContainerFilter containerFilter) + { + List transactionEvents = TRANSACTION_EVENT_CACHE.get(transactionAuditId).second; + List events; + if (transactionEvents.isEmpty()) + { + SimpleFilter filter = new SimpleFilter(); + filter.addCondition(FieldKey.fromParts("TransactionID"), transactionAuditId); + events = AuditLogService.get().getAuditEvents(container, user, SampleTimelineAuditEvent.EVENT_TYPE, filter, null, containerFilter); + } + else + { + events = transactionEvents.stream() + .filter(SampleTimelineAuditEvent.class::isInstance) + .map(SampleTimelineAuditEvent.class::cast) + .toList(); + } + Map dataTypeRowCounts = new HashMap<>(); + List sampleIds = new ArrayList<>(); + events.forEach(event -> { + dataTypeRowCounts.merge(event.getSampleTypeId(), 1L, Long::sum); + sampleIds.add(event.getSampleId()); + }); + return Pair.of(sampleIds, dataTypeRowCounts); + } + + public Pair, Map> getTransactionSourceIds(long transactionAuditId, User user, Container container, @Nullable ContainerFilter containerFilter) + { + List lsids = new ArrayList<>(); + List sourceIds = new ArrayList<>(); + Map dataTypeRowCounts = new HashMap<>(); + List transactionEvents = TRANSACTION_EVENT_CACHE.get(transactionAuditId).second; + List detailedEvents = transactionEvents.isEmpty() + ? QueryService.get().getQueryUpdateAuditRecords(user, container, transactionAuditId, containerFilter) + : transactionEvents.stream() + .filter(DetailedAuditTypeEvent.class::isInstance) + .map(DetailedAuditTypeEvent.class::cast) + .toList(); + + detailedEvents.forEach(event -> { + if (event.getNewRecordMap() != null) + { + Map newRecord = new CaseInsensitiveHashMap<>(AbstractAuditTypeProvider.decodeFromDataMap(event.getNewRecordMap())); + if (newRecord.containsKey("RowId") && !StringUtils.isEmpty(newRecord.get("RowId"))) + sourceIds.add(Long.valueOf(newRecord.get("RowId"))); + else if (newRecord.containsKey("LSID") && !StringUtils.isEmpty(newRecord.get("LSID"))) + lsids.add(newRecord.get("LSID")); + + if (newRecord.containsKey("ClassId") && !StringUtils.isEmpty(newRecord.get("ClassId"))) + { + Long classId = Long.valueOf(newRecord.get("ClassId")); + dataTypeRowCounts.merge(classId, 1L, Long::sum); + } + } + }); + if (!lsids.isEmpty()) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("LSID"), lsids, CompareType.IN); + TableSelector selector = new TableSelector(ExperimentService.get().getTinfoData(), Collections.singleton("RowId"), filter, null); + sourceIds.addAll(selector.getArrayList(Long.class)); + } + return Pair.of(sourceIds, dataTypeRowCounts); + } +}