From 9d52279f4e1906da4c3e14b0d59961af4958cf6d Mon Sep 17 00:00:00 2001 From: Tom Page Date: Wed, 22 Feb 2017 07:45:18 +0000 Subject: [PATCH 1/7] MNT-17512 Add auditing of RM permission changes. --- .../messages/audit-service.properties | 1 + .../rm-service-context.xml | 1 + .../audit/RecordsManagementAuditService.java | 92 +++++++++---------- .../RecordsManagementAuditServiceImpl.java | 14 +-- .../FilePlanPermissionServiceImpl.java | 78 ++++++++++++++++ 5 files changed, 134 insertions(+), 52 deletions(-) diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties index f5cd13d5a1..de68b59a54 100644 --- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties +++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties @@ -14,4 +14,5 @@ rm.audit.audit-clear=Audit Clear rm.audit.audit-view=Audit View rm.audit.trail-file-fail=Can't generate audit report. rm.audit.audit-report=Audit Report +rm.audit.set-permission=Set Permission recordable-version-config=Auto-Declare Options \ No newline at end of file diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml index 08227765ad..b4dd973238 100644 --- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml +++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml @@ -454,6 +454,7 @@ + diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java index f47eb7f26d..859acc7d34 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java @@ -39,18 +39,18 @@ import org.alfresco.service.namespace.QName; /** * Records management audit service. - * + * * @author Gavin Cornwell */ public interface RecordsManagementAuditService extends RecordsManagementAuditServiceDeprecated { public enum ReportFormat { HTML, JSON } - - - + + + /** * Retrieves a list of audit events. - * + * * @return List of audit events */ List getAuditEvents(); @@ -60,49 +60,49 @@ public interface RecordsManagementAuditService extends RecordsManagementAuditSer *

* Creates an instance of a simple audit event and registers it with * the service. - * + * * @param name name of audit event * @param label display label of audit event */ void registerAuditEvent(String name, String label); - + /** * Register audit event. - * + * * @param auditEvent audit event */ void registerAuditEvent(AuditEvent auditEvent); - + /** * Audits an event, assumes no properties where modified and that the event should not be audited - * immediately. - * + * immediately. + * * @param nodeRef node reference * @param eventName event name */ - void auditEvent(NodeRef nodeRef, + void auditEvent(NodeRef nodeRef, String eventName); - + /** * Audits an event, assumes that the event should not be audited immediately and not be removed if no property is changed. - * + * * @param nodeRef node reference * @param eventName event name - * @param before property values before event - * @param after property values after event + * @param before property values before event (this must be modifiable and may be changed by the method). + * @param after property values after event (this must be modifiable and may be changed by the method). */ void auditEvent(NodeRef nodeRef, String eventName, Map before, Map after); - + /** * Audit event, assumes not to be removed if no property is changed. - * + * * @param nodeRef node reference * @param eventName event name - * @param before property values before event - * @param after property values after event + * @param before property values before event (this must be modifiable and may be changed by the method). + * @param after property values after event (this must be modifiable and may be changed by the method). * @param immediate true if event is to be audited immediately, false otherwise */ void auditEvent(NodeRef nodeRef, @@ -110,14 +110,14 @@ public interface RecordsManagementAuditService extends RecordsManagementAuditSer Map before, Map after, boolean immediate); - + /** * Audit event. - * + * * @param nodeRef node reference * @param eventName event name - * @param before property values before event - * @param after property values after event + * @param before property values before event (this must be modifiable and may be changed by the method). + * @param after property values after event (this must be modifiable and may be changed by the method). * @param immediate true if event is to be audited immediately, false otherwise * @param removeIfNoPropertyChanged true if event is not audited when there are no properties changed, false otherwise */ @@ -127,52 +127,52 @@ public interface RecordsManagementAuditService extends RecordsManagementAuditSer Map after, boolean immediate, boolean removeIfNoPropertyChanged); - + /** * Determines whether the RM audit log is currently enabled. - * + * * @param filePlan file plan * @return true if RM auditing is active false otherwise */ - boolean isAuditLogEnabled(NodeRef filePlan); - + boolean isAuditLogEnabled(NodeRef filePlan); + /** * Start RM auditing. - * + * * @param filePlan file plan */ void startAuditLog(NodeRef filePlan); - + /** * Stop RM auditing. - * + * * @param filePlan file plan - */ + */ void stopAuditLog(NodeRef filePlan); - - + + /** * Clears the RM audit. - * + * * @param filePlan file plan */ void clearAuditLog(NodeRef filePlan); - + /** * Returns the date the RM audit was last started. - * - * @param filePlan file plan + * + * @param filePlan file plan * @return Date the audit was last started */ Date getDateAuditLogLastStarted(NodeRef filePlan); /** * Returns the date the RM audit was last stopped. - * + * * @return Date the audit was last stopped */ Date getDateAuditLogLastStopped(NodeRef filePlan); - + /** * Retrieves a list of audit log entries using the provided parameters * represented by the RecordsManagementAuditQueryParameters instance. @@ -181,13 +181,13 @@ public interface RecordsManagementAuditService extends RecordsManagementAuditSer * object will result in ALL audit log entries for the RM system being * returned. Setting the various parameters effectively filters the full * audit trail. - * + * * @param params Parameters to use to retrieve audit trail (never null) * @param format The format the report should be produced in * @return File containing JSON representation of audit trail */ File getAuditTrailFile(RecordsManagementAuditQueryParameters params, ReportFormat format); - + /** * Retrieves a list of audit log entries using the provided parameters * represented by the RecordsManagementAuditQueryParameters instance. @@ -196,23 +196,23 @@ public interface RecordsManagementAuditService extends RecordsManagementAuditSer * object will result in ALL audit log entries for the RM system being * returned. Setting the various parameters effectively filters the full * audit trail. - * + * * @param params Parameters to use to retrieve audit trail (never null) * @return All entries for the audit trail */ List getAuditTrail(RecordsManagementAuditQueryParameters params); - + /** * Retrieves a list of audit log entries using the provided parameters * represented by the RecordsManagementAuditQueryParameters instance and - * then files the resulting log as an undeclared record in the record folder + * then files the resulting log as an undeclared record in the record folder * represented by the given NodeRef. *

* The parameters are all optional so an empty RecordsManagementAuditQueryParameters * object will result in ALL audit log entries for the RM system being * returned. Setting the various parameters effectively filters the full * audit trail. - * + * * @param params Parameters to use to retrieve audit trail (never null) * @param destination NodeRef representing a record folder in which to file the audit log * @param format The format the report should be produced in diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java index 032e3c5bca..864dd7547e 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java @@ -110,7 +110,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean private static Log logger = LogFactory.getLog(RecordsManagementAuditServiceImpl.class); private static final String ACCESS_AUDIT_CAPABILITY = "AccessAudit"; - + private static final String KEY_RM_AUDIT_NODE_RECORDS = "RMAUditNodeRecords"; protected static final String RM_AUDIT_EVENT_LOGIN_SUCCESS = "Login.Success"; @@ -284,7 +284,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean { this.namespaceService = namespaceService; } - + /** * @param capabilityService capability service */ @@ -292,9 +292,9 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean { this.capabilityService = capabilityService; } - - - + + + /** * @param ignoredAuditProperties @@ -506,6 +506,8 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean @Override public void auditEvent(NodeRef nodeRef, String eventName, Map before, Map after, boolean immediate, boolean removeIfNoPropertyChanged) { + logger.error("DEBUGGING: " + String.valueOf(before)); + logger.error("ALSO : " + String.valueOf(after)); // deal with immediate auditing if required if (immediate) { @@ -929,7 +931,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean // Skip it return true; } - + if(nodeRef != null && nodeService.exists(nodeRef) && !AccessStatus.ALLOWED.equals( capabilityService.getCapabilityAccessState(nodeRef, ACCESS_AUDIT_CAPABILITY))) diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java index b2f11c54d5..972b3a81c0 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java @@ -27,6 +27,7 @@ package org.alfresco.module.org_alfresco_module_rm.security; +import static java.util.Collections.singletonMap; import static org.alfresco.module.org_alfresco_module_rm.security.ExtendedReaderDynamicAuthority.EXTENDED_READER; import static org.alfresco.module.org_alfresco_module_rm.security.ExtendedWriterDynamicAuthority.EXTENDED_WRITER; import static org.alfresco.repo.policy.Behaviour.NotificationFrequency.TRANSACTION_COMMIT; @@ -36,10 +37,15 @@ import static org.alfresco.service.cmr.security.OwnableService.NO_OWNER; import static org.alfresco.util.ParameterCheck.mandatory; import static org.apache.commons.lang.BooleanUtils.isTrue; +import java.io.Serializable; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.audit.event.AuditEvent; import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService; import org.alfresco.module.org_alfresco_module_rm.role.FilePlanRoleService; @@ -50,6 +56,7 @@ import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.policy.annotation.Behaviour; import org.alfresco.repo.policy.annotation.BehaviourBean; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AccessPermission; @@ -75,6 +82,11 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl RMPermissionModel, NodeServicePolicies.OnMoveNodePolicy { + private static final String AUDIT_SET_PERMISSION = "set-permission"; + + /** An namespace to use when constructing QNames to use for auditing changes to permissions. */ + private static final String AUDIT_NAMESPACE = "audit://permissions/"; + /** Permission service */ private PermissionService permissionService; @@ -93,6 +105,9 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl /** File plan service */ private FilePlanService filePlanService; + /** The RM audit service. */ + private RecordsManagementAuditService recordsManagementAuditService; + /** Logger */ private static final Log LOGGER = LogFactory.getLog(FilePlanPermissionServiceImpl.class); @@ -113,6 +128,16 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl NodeServicePolicies.OnMoveNodePolicy.QNAME, TYPE_RECORD_CATEGORY, new JavaBehaviour(this, "onMoveNode", TRANSACTION_COMMIT)); + + AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + recordsManagementAuditService.registerAuditEvent(new AuditEvent(AUDIT_SET_PERMISSION, "rm.audit.set-permission")); + return null; + } + }); } /** @@ -229,6 +254,16 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl this.filePlanService = filePlanService; } + /** + * Set the RM audit service. + * + * @param recordsManagementAuditService The RM audit service. + */ + public void setRecordsManagementAuditService(RecordsManagementAuditService recordsManagementAuditService) + { + this.recordsManagementAuditService = recordsManagementAuditService; + } + /** * @see org.alfresco.module.org_alfresco_module_rm.security.FilePlanPermissionService#setupRecordCategoryPermissions(org.alfresco.service.cmr.repository.NodeRef) */ @@ -342,6 +377,7 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl final boolean hasUserPermission = authenticationUtil.runAs(new AuthenticationUtil.RunAsWork() { + @Override public Boolean doWork() { return getPermissionService().hasPermission(nodeRef, RMPermissionModel.FILING) == AccessStatus.ALLOWED; @@ -352,6 +388,7 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl { authenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork() { + @Override public Void doWork() { getPermissionService().setPermission(nodeRef, user, RMPermissionModel.FILING, true); @@ -367,6 +404,7 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl * @param parent parent node reference * @param nodeRef child node reference */ + @Override public void setupPermissions(final NodeRef parent, final NodeRef nodeRef) { mandatory("parent", parent); @@ -376,6 +414,7 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl { authenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork() { + @Override public Object doWork() { // set inheritance @@ -455,6 +494,7 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl authenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork() { + @Override public Object doWork() { if (nodeService.exists(record) && nodeService.hasAspect(record, aspectTypeQName)) @@ -481,6 +521,7 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl authenticationUtil.runAs(new AuthenticationUtil.RunAsWork() { + @Override public Void doWork() { NodeRef record = sourceAssocRef.getChildRef(); @@ -528,6 +569,7 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl /** * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#setPermission(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String, boolean) */ + @Override public void setPermission(final NodeRef nodeRef, final String authority, final String permission) { ParameterCheck.mandatory("nodeRef", nodeRef); @@ -536,12 +578,19 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl authenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork() { + @Override public Void doWork() { if (canPerformPermissionAction(nodeRef)) { + // Construct a QName so that the authority and permission are visible in the log. + QName auditProperty = QName.createQName(AUDIT_NAMESPACE, authority + "_" + permission); + Map oldPermission = getCurrentPermissionForAuthority(nodeRef, authority, permission, auditProperty); // Set the permission on the node getPermissionService().setPermission(nodeRef, authority, permission, true); + // Add an entry in the audit log. + recordsManagementAuditService.auditEvent(nodeRef, AUDIT_SET_PERMISSION, + oldPermission, new HashMap<>(singletonMap(auditProperty, (Serializable) true))); } else { @@ -556,9 +605,31 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl }); } + /** + * Get the current permission on a node for an authority. + * + * @param nodeRef The node. + * @param authority The authority. + * @param auditProperty The QName used as the key in the returned map. + * @return A map from the audit property to true or false depending on whether the user currently has permission. + */ + private Map getCurrentPermissionForAuthority(NodeRef nodeRef, String authority, String permission, QName auditProperty) + { + Set allSetPermissions = getPermissionService().getAllSetPermissions(nodeRef); + for (AccessPermission setPermission : allSetPermissions) + { + if (setPermission.getAuthority().equals(authority) && setPermission.getPermission().equals(permission)) + { + return new HashMap<>(singletonMap(auditProperty, true)); + } + } + return new HashMap<>(singletonMap(auditProperty, false)); + } + /** * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#deletePermission(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String) */ + @Override public void deletePermission(final NodeRef nodeRef, final String authority, final String permission) { ParameterCheck.mandatory("nodeRef", nodeRef); @@ -567,12 +638,19 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl authenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork() { + @Override public Void doWork() { if (canPerformPermissionAction(nodeRef)) { + // Construct a QName so that the authority and permission are visible in the log. + QName auditProperty = QName.createQName(AUDIT_NAMESPACE, authority + "_" + permission); + Map oldPermission = getCurrentPermissionForAuthority(nodeRef, authority, permission, auditProperty); // Delete permission on this node getPermissionService().deletePermission(nodeRef, authority, permission); + // Add an entry in the audit log. + recordsManagementAuditService.auditEvent(nodeRef, AUDIT_SET_PERMISSION, + oldPermission, new HashMap<>(singletonMap(auditProperty, (Serializable) false))); } else { From 57f15669074b6357b33754d2b03d2741703ad2f8 Mon Sep 17 00:00:00 2001 From: Tom Page Date: Thu, 23 Feb 2017 08:26:49 +0000 Subject: [PATCH 2/7] MNT-17512 Revert debugging change to RecordsManagementAuditServiceImpl.java. --- .../audit/RecordsManagementAuditServiceImpl.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java index 864dd7547e..032e3c5bca 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java @@ -110,7 +110,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean private static Log logger = LogFactory.getLog(RecordsManagementAuditServiceImpl.class); private static final String ACCESS_AUDIT_CAPABILITY = "AccessAudit"; - + private static final String KEY_RM_AUDIT_NODE_RECORDS = "RMAUditNodeRecords"; protected static final String RM_AUDIT_EVENT_LOGIN_SUCCESS = "Login.Success"; @@ -284,7 +284,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean { this.namespaceService = namespaceService; } - + /** * @param capabilityService capability service */ @@ -292,9 +292,9 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean { this.capabilityService = capabilityService; } - - - + + + /** * @param ignoredAuditProperties @@ -506,8 +506,6 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean @Override public void auditEvent(NodeRef nodeRef, String eventName, Map before, Map after, boolean immediate, boolean removeIfNoPropertyChanged) { - logger.error("DEBUGGING: " + String.valueOf(before)); - logger.error("ALSO : " + String.valueOf(after)); // deal with immediate auditing if required if (immediate) { @@ -931,7 +929,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean // Skip it return true; } - + if(nodeRef != null && nodeService.exists(nodeRef) && !AccessStatus.ALLOWED.equals( capabilityService.getCapabilityAccessState(nodeRef, ACCESS_AUDIT_CAPABILITY))) From f83a6d7373cf309f0619a9ed91139b789722a866 Mon Sep 17 00:00:00 2001 From: Tom Page Date: Thu, 23 Feb 2017 08:32:11 +0000 Subject: [PATCH 3/7] MNT-17512 Fix compilation for Java 7. --- .../security/FilePlanPermissionServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java index 972b3a81c0..0667fcd96b 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java @@ -620,10 +620,10 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl { if (setPermission.getAuthority().equals(authority) && setPermission.getPermission().equals(permission)) { - return new HashMap<>(singletonMap(auditProperty, true)); + return new HashMap<>(singletonMap(auditProperty, (Serializable) true)); } } - return new HashMap<>(singletonMap(auditProperty, false)); + return new HashMap<>(singletonMap(auditProperty, (Serializable) false)); } /** From 6616f0c5728d86aa2d6f14143838bc83a7100541 Mon Sep 17 00:00:00 2001 From: Tom Page Date: Thu, 23 Feb 2017 10:51:46 +0000 Subject: [PATCH 4/7] MNT-17512 Allow partial updates to audit events. This is required so that the multiple calls to add permission and remove permission do not overwrite each other within the transaction (e.g. if there are permissions for multiple authorities being added, or if a permission is being changed for a user). --- .../audit/RecordsManagementAuditService.java | 15 +++ .../RecordsManagementAuditServiceImpl.java | 109 +++++++++++++----- .../FilePlanPermissionServiceImpl.java | 27 +++-- 3 files changed, 112 insertions(+), 39 deletions(-) diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java index 859acc7d34..77eada74f6 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java @@ -128,6 +128,21 @@ public interface RecordsManagementAuditService extends RecordsManagementAuditSer boolean immediate, boolean removeIfNoPropertyChanged); + /** + * Supply incremental changes as part of an event. This will either create a new event or update the existing details to put any supplied properties into the map. + * + * @param nodeRef node reference + * @param eventName event name + * @param before additional property values before event (this must be modifiable and may be changed by the method). + * @param after additional property values after event (this must be modifiable and may be changed by the method). + * @param removeIfNoPropertyChanged true if event is not audited when there are no properties changed, false otherwise + */ + void auditOrUpdateEvent(NodeRef nodeRef, + String eventName, + Map before, + Map after, + boolean removeIfNoPropertyChanged); + /** * Determines whether the RM audit log is currently enabled. * diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java index 032e3c5bca..977bbc73ac 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java @@ -110,7 +110,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean private static Log logger = LogFactory.getLog(RecordsManagementAuditServiceImpl.class); private static final String ACCESS_AUDIT_CAPABILITY = "AccessAudit"; - + private static final String KEY_RM_AUDIT_NODE_RECORDS = "RMAUditNodeRecords"; protected static final String RM_AUDIT_EVENT_LOGIN_SUCCESS = "Login.Success"; @@ -284,7 +284,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean { this.namespaceService = namespaceService; } - + /** * @param capabilityService capability service */ @@ -292,9 +292,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean { this.capabilityService = capabilityService; } - - - + /** * @param ignoredAuditProperties @@ -514,40 +512,89 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean } else { - Set auditDetails = TransactionalResourceHelper.getSet(KEY_RM_AUDIT_NODE_RECORDS); - AlfrescoTransactionSupport.bindListener(txnListener); - // RM-936: Eliminate multiple audit maps from being generated when events with the same name are required to be fired multiple times in the same transaction. // Check if auditDetails already contains an auditedNode with the same combination of nodeRef and eventName. - boolean auditNodeAlreadyExists = false; - for (RMAuditNode existingRMAuditNode : auditDetails) + RMAuditNode existingEventNode = findExistingEventNode(nodeRef, eventName); + if (existingEventNode != null) { - if (existingRMAuditNode.getNodeRef().equals(nodeRef) && existingRMAuditNode.getEventName().equals(eventName)) - { - // If there exists such an auditNode, update its 'after' properties with the latest set of properties and leave its 'before' properties unchanged so that it - // retains the original set of properties. The first 'before' and last 'after' will be diff'ed when comes to building the auditMap later when the transaction - // commits. - existingRMAuditNode.setNodePropertiesAfter(after); - auditNodeAlreadyExists = true; - break; - } + // If there exists such an auditNode, update its 'after' properties with the latest set of properties and leave its 'before' properties unchanged so that it + // retains the original set of properties. The first 'before' and last 'after' will be diff'ed when comes to building the auditMap later when the transaction + // commits. + existingEventNode.setNodePropertiesAfter(after); } - - if (!auditNodeAlreadyExists) + else { - // Create a new auditNode if it doesn't already exist - RMAuditNode auditedNode = new RMAuditNode(); - auditedNode.setNodeRef(nodeRef); - auditedNode.setEventName(eventName); - auditedNode.setNodePropertiesBefore(before); - auditedNode.setNodePropertiesAfter(after); - auditedNode.setRemoveIfNoPropertyChanged(removeIfNoPropertyChanged); - - auditDetails.add(auditedNode); + createAuditEventInTransaction(nodeRef, eventName, before, after, removeIfNoPropertyChanged); } } } + /** {@inheritDoc} */ + @Override + public void auditOrUpdateEvent(NodeRef nodeRef, String eventName, Map before, + Map after, boolean removeIfNoPropertyChanged) + { + RMAuditNode existingEventNode = findExistingEventNode(nodeRef, eventName); + if (existingEventNode != null) + { + // Update the existing event to include all the new properties. + existingEventNode.getNodePropertiesBefore().putAll(before); + existingEventNode.getNodePropertiesAfter().putAll(after); + } + else + { + createAuditEventInTransaction(nodeRef, eventName, before, after, removeIfNoPropertyChanged); + } + } + + /** + * Create a new audit event for this transaction. + * + * @param nodeRef The node the audit message is about. + * @param eventName The event. + * @param before The before property map to use. + * @param after The after property map to use. + * @param removeIfNoPropertyChanged Whether to remove the event if no properties have changed. + */ + private void createAuditEventInTransaction(NodeRef nodeRef, String eventName, Map before, + Map after, boolean removeIfNoPropertyChanged) + { + // Create a new auditNode. + RMAuditNode auditedNode = new RMAuditNode(); + auditedNode.setNodeRef(nodeRef); + auditedNode.setEventName(eventName); + auditedNode.setNodePropertiesBefore(before); + auditedNode.setNodePropertiesAfter(after); + auditedNode.setRemoveIfNoPropertyChanged(removeIfNoPropertyChanged); + + // Add it to the transaction. + Set auditDetails = TransactionalResourceHelper.getSet(KEY_RM_AUDIT_NODE_RECORDS); + auditDetails.add(auditedNode); + } + + /** + * Find an audit node if it already exists for the transaction. + * + * @param nodeRef The node the event is against. + * @param eventName The name of the event. + * @param auditDetails The complete set of events for the transaction. + * @return The pre-existing event node, or null if none exists. + */ + private RMAuditNode findExistingEventNode(NodeRef nodeRef, String eventName) + { + AlfrescoTransactionSupport.bindListener(txnListener); + Set auditDetails = TransactionalResourceHelper.getSet(KEY_RM_AUDIT_NODE_RECORDS); + RMAuditNode existingEventNode = null; + for (RMAuditNode existingRMAuditNode : auditDetails) + { + if (existingRMAuditNode.getNodeRef().equals(nodeRef) && existingRMAuditNode.getEventName().equals(eventName)) + { + existingEventNode = existingRMAuditNode; + } + } + return existingEventNode; + } + /** * Helper method to build audit map * @@ -929,7 +976,7 @@ public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean // Skip it return true; } - + if(nodeRef != null && nodeService.exists(nodeRef) && !AccessStatus.ALLOWED.equals( capabilityService.getCapabilityAccessState(nodeRef, ACCESS_AUDIT_CAPABILITY))) diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java index 0667fcd96b..b691190b61 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java @@ -583,14 +583,13 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl { if (canPerformPermissionAction(nodeRef)) { - // Construct a QName so that the authority and permission are visible in the log. - QName auditProperty = QName.createQName(AUDIT_NAMESPACE, authority + "_" + permission); + QName auditProperty = constructAuditEventName(authority, permission); Map oldPermission = getCurrentPermissionForAuthority(nodeRef, authority, permission, auditProperty); // Set the permission on the node getPermissionService().setPermission(nodeRef, authority, permission, true); // Add an entry in the audit log. - recordsManagementAuditService.auditEvent(nodeRef, AUDIT_SET_PERMISSION, - oldPermission, new HashMap<>(singletonMap(auditProperty, (Serializable) true))); + recordsManagementAuditService.auditOrUpdateEvent(nodeRef, AUDIT_SET_PERMISSION, oldPermission, + new HashMap<>(singletonMap(auditProperty, (Serializable) true)), true); } else { @@ -643,14 +642,13 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl { if (canPerformPermissionAction(nodeRef)) { - // Construct a QName so that the authority and permission are visible in the log. - QName auditProperty = QName.createQName(AUDIT_NAMESPACE, authority + "_" + permission); + QName auditProperty = constructAuditEventName(authority, permission); Map oldPermission = getCurrentPermissionForAuthority(nodeRef, authority, permission, auditProperty); // Delete permission on this node getPermissionService().deletePermission(nodeRef, authority, permission); // Add an entry in the audit log. - recordsManagementAuditService.auditEvent(nodeRef, AUDIT_SET_PERMISSION, - oldPermission, new HashMap<>(singletonMap(auditProperty, (Serializable) false))); + recordsManagementAuditService.auditOrUpdateEvent(nodeRef, AUDIT_SET_PERMISSION, oldPermission, + new HashMap<>(singletonMap(auditProperty, (Serializable) false)), true); } else { @@ -665,6 +663,19 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl }); } + /** + * Construct a QName so that the authority and permission are visible in the log. + * + * @param authority The authority whose permission is being changed. + * @param permission The name of the permission being changed. + * @return A QName such that the local name will make sense to the end user. + */ + private QName constructAuditEventName(String authority, String permission) + { + QName auditProperty = QName.createQName(AUDIT_NAMESPACE, permission + " " + authority); + return auditProperty; + } + private boolean canPerformPermissionAction(NodeRef nodeRef) { return isFilePlanContainer(nodeRef) || isRecordFolder(nodeRef) || isRecord(nodeRef) || isTransfer(nodeRef); From 7dab46ad40d8c1a31284186983458e258b9ae1fa Mon Sep 17 00:00:00 2001 From: Tom Page Date: Thu, 23 Feb 2017 11:40:04 +0000 Subject: [PATCH 5/7] MNT-17512 Add audit events for changes to inherited permissions. --- .../extended-repository-context.xml | 1 + .../messages/audit-service.properties | 2 + .../FilePlanPermissionServiceImpl.java | 1 + .../impl/ExtendedPermissionServiceImpl.java | 78 +++++++++++++++---- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/extended-repository-context.xml b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/extended-repository-context.xml index d2b0dc13a7..44841fde9e 100644 --- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/extended-repository-context.xml +++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/extended-repository-context.xml @@ -113,6 +113,7 @@ + diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties index de68b59a54..a9c90dfba8 100644 --- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties +++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties @@ -15,4 +15,6 @@ rm.audit.audit-view=Audit View rm.audit.trail-file-fail=Can't generate audit report. rm.audit.audit-report=Audit Report rm.audit.set-permission=Set Permission +rm.audit.enable-inherit-permission=Enable Inherit Permissions +rm.audit.disable-inherit-permission=Disable Inherit Permissions recordable-version-config=Auto-Declare Options \ No newline at end of file diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java index b691190b61..277863f8b5 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/security/FilePlanPermissionServiceImpl.java @@ -82,6 +82,7 @@ public class FilePlanPermissionServiceImpl extends ServiceBaseImpl RMPermissionModel, NodeServicePolicies.OnMoveNodePolicy { + /** An audit key for the set permission event. */ private static final String AUDIT_SET_PERMISSION = "set-permission"; /** An namespace to use when constructing QNames to use for auditing changes to permissions. */ diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/repo/security/permissions/impl/ExtendedPermissionServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/repo/security/permissions/impl/ExtendedPermissionServiceImpl.java index 09ff3073bb..fd87bd94b9 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/repo/security/permissions/impl/ExtendedPermissionServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/repo/security/permissions/impl/ExtendedPermissionServiceImpl.java @@ -35,6 +35,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.audit.event.AuditEvent; import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; @@ -42,6 +44,8 @@ import org.alfresco.module.org_alfresco_module_rm.role.FilePlanRoleService; import org.alfresco.module.org_alfresco_module_rm.security.ExtendedReaderDynamicAuthority; import org.alfresco.module.org_alfresco_module_rm.security.ExtendedWriterDynamicAuthority; import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.permissions.AccessControlEntry; import org.alfresco.repo.security.permissions.AccessControlList; import org.alfresco.repo.security.permissions.processor.PermissionPostProcessor; @@ -65,15 +69,40 @@ import org.springframework.context.ApplicationEvent; public class ExtendedPermissionServiceImpl extends PermissionServiceImpl implements ExtendedPermissionService { - /** Writers simple cache */ + /** An audit key for the enable permission inheritance event. */ + private static final String AUDIT_ENABLE_INHERIT_PERMISSION = "enable-inherit-permission"; + /** An audit key for the disable permission inheritance event. */ + private static final String AUDIT_DISABLE_INHERIT_PERMISSION = "disable-inherit-permission"; + + /** Writers simple cache */ protected SimpleCache> writersCache; /** File plan service */ private FilePlanService filePlanService; - + /** Permission processor registry */ private PermissionProcessorRegistry permissionProcessorRegistry; + /** The RM audit service. */ + private RecordsManagementAuditService recordsManagementAuditService; + + /** {@inheritDoc} Register the audit events. */ + @Override + public void init() + { + super.init(); + AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + recordsManagementAuditService.registerAuditEvent(new AuditEvent(AUDIT_ENABLE_INHERIT_PERMISSION, "rm.audit.enable-inherit-permission")); + recordsManagementAuditService.registerAuditEvent(new AuditEvent(AUDIT_DISABLE_INHERIT_PERMISSION, "rm.audit.disable-inherit-permission")); + return null; + } + }); + } + /** * Gets the file plan service * @@ -93,17 +122,27 @@ public class ExtendedPermissionServiceImpl extends PermissionServiceImpl { this.filePlanService = filePlanService; } - + /** * Sets the permission processor registry - * + * * @param permissionProcessorRegistry the permissions processor registry */ - public void setPermissionProcessorRegistry(PermissionProcessorRegistry permissionProcessorRegistry) + public void setPermissionProcessorRegistry(PermissionProcessorRegistry permissionProcessorRegistry) { this.permissionProcessorRegistry = permissionProcessorRegistry; } + /** + * Set the RM audit service. + * + * @param recordsManagementAuditService The RM audit service. + */ + public void setRecordsManagementAuditService(RecordsManagementAuditService recordsManagementAuditService) + { + this.recordsManagementAuditService = recordsManagementAuditService; + } + /** * @see org.alfresco.repo.security.permissions.impl.PermissionServiceImpl#setAnyDenyDenies(boolean) */ @@ -146,40 +185,40 @@ public class ExtendedPermissionServiceImpl extends PermissionServiceImpl public AccessStatus hasPermission(NodeRef nodeRef, String perm) { AccessStatus result = AccessStatus.UNDETERMINED; - + // permission pre-processors List preProcessors = permissionProcessorRegistry.getPermissionPreProcessors(); - for (PermissionPreProcessor preProcessor : preProcessors) + for (PermissionPreProcessor preProcessor : preProcessors) { // pre process permission result = preProcessor.process(nodeRef, perm); - + // veto if denied if (AccessStatus.DENIED.equals(result)) { return result; } } - + // evaluate permission result = hasPermissionImpl(nodeRef, perm); - + // permission post-processors List postProcessors = permissionProcessorRegistry.getPermissionPostProcessors(); - for (PermissionPostProcessor postProcessor : postProcessors) + for (PermissionPostProcessor postProcessor : postProcessors) { // post process permission result = postProcessor.process(result, nodeRef, perm); - } - + } + return result; } - + /** * Implementation of hasPermission method call. *

* Separation also convenient for unit testing. - * + * * @param nodeRef node reference * @param perm permission * @return {@link AccessStatus} access status result @@ -275,6 +314,7 @@ public class ExtendedPermissionServiceImpl extends PermissionServiceImpl * @param aclId * @return */ + @Override public Set getReadersDenied(Long aclId) { AccessControlList acl = aclDaoComponent.getAccessControlList(aclId); @@ -314,6 +354,7 @@ public class ExtendedPermissionServiceImpl extends PermissionServiceImpl /** * @see org.alfresco.repo.security.permissions.impl.ExtendedPermissionService#getWriters(java.lang.Long) */ + @Override public Set getWriters(Long aclId) { AccessControlList acl = aclDaoComponent.getAccessControlList(aclId); @@ -363,7 +404,12 @@ public class ExtendedPermissionServiceImpl extends PermissionServiceImpl setPermission(nodeRef, ExtendedWriterDynamicAuthority.EXTENDED_WRITER, RMPermissionModel.FILING, true); setPermission(nodeRef, adminRole, RMPermissionModel.FILING, true); } - super.setInheritParentPermissions(nodeRef, inheritParentPermissions); + if (inheritParentPermissions != super.getInheritParentPermissions(nodeRef)) + { + super.setInheritParentPermissions(nodeRef, inheritParentPermissions); + String auditEvent = (inheritParentPermissions ? AUDIT_ENABLE_INHERIT_PERMISSION : AUDIT_DISABLE_INHERIT_PERMISSION); + recordsManagementAuditService.auditEvent(nodeRef, auditEvent); + } } private String getAdminRole(NodeRef nodeRef) From ac65ee775873e609177975e2376a4dc2c06f3ce6 Mon Sep 17 00:00:00 2001 From: Tom Page Date: Fri, 24 Feb 2017 08:05:11 +0000 Subject: [PATCH 6/7] MNT-17512 Minor fixes after review. Add new service method to security bean. Add early exit from audit event finding loop. Tidy javadoc and remove some blank lines. --- .../module/org_alfresco_module_rm/rm-service-context.xml | 1 + .../audit/RecordsManagementAuditService.java | 3 --- .../audit/RecordsManagementAuditServiceImpl.java | 6 ++---- .../permissions/impl/ExtendedPermissionServiceImpl.java | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml index b4dd973238..a39e06c4f0 100644 --- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml +++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml @@ -970,6 +970,7 @@ auditDetails = TransactionalResourceHelper.getSet(KEY_RM_AUDIT_NODE_RECORDS); - RMAuditNode existingEventNode = null; for (RMAuditNode existingRMAuditNode : auditDetails) { if (existingRMAuditNode.getNodeRef().equals(nodeRef) && existingRMAuditNode.getEventName().equals(eventName)) { - existingEventNode = existingRMAuditNode; + return existingRMAuditNode; } } - return existingEventNode; + return null; } /** diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/repo/security/permissions/impl/ExtendedPermissionServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/repo/security/permissions/impl/ExtendedPermissionServiceImpl.java index fd87bd94b9..9ac005bf74 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/repo/security/permissions/impl/ExtendedPermissionServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/repo/security/permissions/impl/ExtendedPermissionServiceImpl.java @@ -209,7 +209,7 @@ public class ExtendedPermissionServiceImpl extends PermissionServiceImpl { // post process permission result = postProcessor.process(result, nodeRef, perm); - } + } return result; } From 6a756f9f5d139d999991da7d17e055a55ef32110 Mon Sep 17 00:00:00 2001 From: Tom Page Date: Fri, 24 Feb 2017 10:05:56 +0000 Subject: [PATCH 7/7] MNT-17512 Update audit event strings after review from UA. --- .../org_alfresco_module_rm/messages/audit-service.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties index a9c90dfba8..d05748fb40 100644 --- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties +++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties @@ -15,6 +15,6 @@ rm.audit.audit-view=Audit View rm.audit.trail-file-fail=Can't generate audit report. rm.audit.audit-report=Audit Report rm.audit.set-permission=Set Permission -rm.audit.enable-inherit-permission=Enable Inherit Permissions -rm.audit.disable-inherit-permission=Disable Inherit Permissions +rm.audit.enable-inherit-permission=Inherited Permissions Switched On +rm.audit.disable-inherit-permission=Inherited Permissions Switched Off recordable-version-config=Auto-Declare Options \ No newline at end of file