This document only describes the new auditing framework introduced with Intershop 7.1.
Currently, auditing for PA-DSS is not based on the new auditing framework.
Auditing is the feature to produce an audit trail.
Definition from Wikipedia: An audit trail (or audit log) is a security-relevant chronological record, set of records, or destination and source of records that provide documentary evidence of the sequence of activities that have affected at any time a specific operation, procedure, or event.
The basic ideas for the auditing framework are:
For common questions, please refer to the corresponding cookbooks: Cookbook - Auditing (7.1 - 7.2) and Cookbook - Auditing.
See also: Reference - Auditing, or former versions Reference - Auditing 7.3, or Reference - Auditing (7.1 - 7.2).
The diagram shows the main components involved in the processing of audit messages, which are:
The audit engine is the central component of the auditing framework as it delivers the functionality to submit audit messages using the business code, to filter them and to route them to targets.
In order to guarantee a consistent and well-defined audit trail where the behavior is predictable also in uncommon situations like system crashes, the audit transaction is used as basic concept.
The following cases have to be considered:
The term database transaction here only stands for the database transaction managed by the ORM framework, not any other opened transaction even on the same database.
The basic patterns for integrating auditing into code should look as follows:
public void doBusiness() { // Audit transaction managed completely by the business code try(ManuallyManagedAuditTransaction a = getAuditEngine().beginManuallyManagedTransaction(aMarker)) { try { a.audit(aMessage); performBusinessActions(); } catch(Throwable ex) { a.rollback(); throw ex; } a.commit(); } }
public void doBusiness() { // Audit transaction bound the ORM transaction try(AuditTransaction a = getAuditEngine().beginORMBoundTransaction(aMarker)) { a.audit(aMessage); performBusinessActions(); } }
Introducing the AuditTransaction
:
AuditEngine
to return an optimized implementation depending on the featured actual configured audit targetsAutoCloseable
feature of Java 7The audit transaction automatically writes additional audit events that mark the beginning and end of the transaction within an audit trail if no atomic database transactions are available. In this way, a business action can result in multiple audit messages where the first one says that an action starts, is then followed by updates and ends with finish or fail. Five kinds of events exist: BEGIN, UPDATE, COMMIT, ROLLBACK, and FAIL. If both the business operation auditing and the auditing target are based on the ORM transaction, only the UPDATE events are created and stored in the target.
Semantically, it makes no difference whether the audit message is submitted before or after actually performing the business operation. The only difference is that if something fails during the business, the audit trail will have entries about attempts that failed, which is not the case if done in the opposite order.
The audit engine always returns the same ORM-bound audit transaction if BEGIN is called within the same active ORM transaction. Manually managed transactions are always newly created.
Auditing from pipelines:
Markers are used to build groups of audited messages/events. They make it possible to easily select audit messages for certain business cases to be routed to the desired targets. For PA-DSS certification, a defined set of audit messages is required as well. These sets can be explicitly defined with markers, so that code changes that may influence the certification are more transparent to the developers.
When opening a transaction, marker names should be used that identify the business operation as unique as possible.
Note
The ORM transaction listening auditor always uses the same marker "audit.marker.ORMAuditor"
.
If multiple calls to a begin method() at the audit engine return the same transaction, the transaction will contain all markers of all begin calls. So if for the ORM transaction listening auditor more uniqueness is required, the business code needs to be enriched, so that a uniquely named audit transaction opened before the ORM transaction is committed.
The markers referred to the begin method in the calls have to be known to the marker registry assigned to the audit engine. This is achieved by declaring markers via the component framework. Include relations between markers also have to be declared via the component framework, see section Configuration.
The set of markers forms an acyclic directed graph, representing a containment relationship. In this way, for filtering a composite marker can be set to the marker filter and all events with markers belonging to this composite marker are accepted. Actually, not the composite marker held references to all included markers, but the included markers held references to their direct includes to composites. In this way, the toString()
method of a marker does not only print its name, but also the name of composites where it is included itself.
Pipeline markers
Since within the programming model of the Intershop solution the business operations are mostly started via pipelines, the current pipeline and start node name can be regarded as an identifier for the business operation and as a good marker name. For this case, the built-in marker registry has a special support.
pipeline.
<cartridge>.
<pipeline>.
<start node>.This example means that the marker:
The business application submits an AuditMessage
via the audit transaction. The transaction enriches the message with additional contextual information and passes them as an AuditEvent
to the engine for further processing (filtering and routing to the targets).
The AuditMessage
can hold the following information:
AuditMessagePayload
.Note
Audit messages should be as self-explanatory as possible, since they can live longer than the business objects involved.
The AuditEvent
contains:
The audit engine routes to events to the configured targets. A target is responsible for storing the audit events. The cartidge bc_auditing comes only with a simple LogAuditTarget
. That is mainly only for testing purposes since it simply generates log messages with the unstructured AuditEvent
. Since logging hides errors, it is not strictly guaranteed that the produced log contains all events for all committed transactions. The cartridge bc_auditing_orm provides a strict implementation that writes into the ORM database.
The AuditTarget
interface:
public interface AuditTarget { /** * Returns true, if ORM transaction is supported, otherwise false. * @return returns true, if ORM transaction is supported, otherwise false. */ public boolean supportsORMTransaction(); /** * Appends action message to an transaction identified by the transaction id. * * @param auditEvent * @throws AuditException */ public void appendMessage(AuditEvent auditEvent) throws AuditException; /** * A target can have a filter, that decides if an audit event is to be logged by this target. * The filter is executed by the AuditEngine. * * @return The assigned filter or null if there is no assigned. */ public AuditEventFilter getFilter(); }
The method getFilter()
is already implemented in AbstractAuditTarget
that should be the preferred super class for custom implementation.
If the method appendMessage()
returns an exception, this exception will not be caught so that the business operation fails since auditing failed.
Via the method supportsORMTransaction()
the target tells the system if it writes to the ORM database. In this way, the auditing framework prevents transaction boundary events to be sent to this target if the audit transaction is also bound to the ORM transaction.
The event numbers passed to each target are guaranteed to be in sequence without holes even if some generated events have been filtered out.
Audit events can be filtered out for each target individually. To achieve this, the AuditEventFilter
interface provides the method accept
where each implementation can test on any attribute of the full AuditEvent
. The available AbstractAuditEventFilter
provides the feature to construct chains of filters.
Currently, the implementation AuditMarkerFilter
is provided.
Note
When filters are used depending on the actual filters, the audit transaction can be seen only partially at a target.
<IS_SHARE>/system/config/cartridges/bc_auditing.properties contains:
intershop.auditing.enable=false
This property has to be set to true
.
Note
With Intershop Commere Management 7.10, the bc_auditing.properties were moved from \share\system\config\cartridges to bc_auditing\src\main\resources\cartridges. Therefore, set the global property in the appserver.properties instead.
The audit engine itself is registered as application type-specific via the component framework (cartridge bc_auditing):
<implementation name="AuditEngine" ...> <requires name="markerRegistry" contract="com.intershop.component.auditing.internal.core.AuditMarkerRegistry" cardinality="1..1"/> <requires name="target" contract="com.intershop.component.auditing.capi.core.target.AuditTarget" cardinality="0..n"/> </implementation> <instance name="AuditEngine" with="AuditEngine" scope="app"> <fulfill requirement="markerRegistry"> <instance name="AuditMarkerRegistry" with="AuditMarkerRegistry" scope="app"/> </fulfill> </instance>
In this way, each application type holds its own engine instance with its own configuration. The configuration for the audit engine consists of a marker registry and a list of targets where each target also can hold an event filter.
With this the audit engine can be retrieved like this:
AppContext c = AppContextUtil.getCurrentAppContext(); AuditEngine auditEngine = c.getApp().getNamedObject("AuditEngine");
Declaring markers, assigning them to a registry, and defining an include relation looks as follows:
<fulfill requirement="marker" of="AuditMarkerRegistry"> <!-- the name of the instance is also used as the name of the marker itself --> <instance name="audit.marker.PADSS" with="AuditMarker" scope="global"/> <instance name="audit.marker.Login" with="AuditMarker" scope="global"/> </fulfill> <fulfill requirement="include" of="audit.marker.PADSS" with="audit.marker.Login"/>
Only markers known to the marker registry of the audit engine can be used to open a transaction.
Adding a target to the engine that contains a marker filter itself:
<instance name="pipeline.*.ViewPromotionAttachmentUpload.AddDirectory" with="AuditMarker" scope="global"/> <fulfill requirement="target" of="AuditEngine"> <instance with="AuditORMTarget"> <fulfill requirement="filter"> <instance with="AuditEventFilter.MarkerFilter"> <fulfill requirement="marker" with="pipeline.*.ViewPromotionAttachmentUpload.AddDirectory"/> </instance> </fulfill> </instance> </fulfill>
Auditing should produce messages using terms for business users. But actually extending already existing code at business level with auditing features is a time-consuming and expensive task, especially as long as no listener concept for the business object level exists. As a workaround to this, the listener mechanism at the persistence level (ORM) can be used.
The Intershop solution provides the ORM auditor in the bc_auditing cartridge, which listens on ORM transactions for changes on a configurable set of persistent object types. If a change occurs for the type registered, the so-called AuditMessageProducer
is responsible for creating a business user-understandable AuditMessage
. This message is sent by the ORM auditor to the audit engine.
Features:
com.intershop.component.auditing.capi.ormauditor.ContextFilter
)In bc_auditing, the ORMAuditor instance is created via the component framework
<instance name="ORMAuditor" with="ORMAuditor" scope="global"> <fulfill requirement="messageProducers"> <instance name="ORMAuditorMessageProducerRegistry" with="ORMAuditApplicationTypeMessageProducerAssignments" scope="global"/> </fulfill> <fulfill requirement="contextFilter"> <instance name="ORMAuditorContextFilter" with="ORMAuditor.RequestContextFilter" scope="global"/> </fulfill> </instance>
The ORMAuditor opens one new ORM-bound audit transaction per ORM transaction. Actually, opening the audit transaction is delayed up to the first real message to be audited.
The ORMAuditor always opens its own audit transaction with the marker audit.marker.ORMAuditor
.
The task of the audit message producers is to translate the raw data delivered by the ORMObjectListener
into a meaningful audit message.
public interface AuditMessageProducer { /** * Create an audit message when an action on an object is performed. * * @return Null to signal that there is no message to be logged. */ AuditMessage createAuditMessage(ORMObject object, ActionType ormAction, Map<AttributeDescription, Object> previousValues); /** * Return the set of ORMObject types that are supported by this message producer. Preferably only a small set of leaf classes. */ Collection<Class<? extends ORMObject>> getSupportedTypes(); /** * Return the actual attributes to be observed for auditing for the supported types. */ Collection<? extends AttributeDescription> getAuditAttributes(Class<? extends ORMObject> ormType); }
Implementations should always subclass from com.intershop.component.auditing.capi.ormauditor.AbstractAuditMessageProducer
. It already provides a meaningful implementation for getAuditAttributes()
. For concrete delivered instances see Reference - Auditing.
Instances are registered via the component framework. They can be registered globally (for all applications) or as application-specific instances.
<fulfill requirement="assignment" of="ORMAuditorMessageProducerRegistry"> <instance with="ORMAuditApplicationTypeMessageProducerAssignment"> <!-- the app is optional --> <fulfill requirement="app" with="intershop.B2CBackoffice" /> <fulfill requirement="messageProducer"> <instance with="ORMAuditor.JobConfigurationAuditMessageProducer"/> <instance with="ORMAuditor.JobTimeConditionAuditMessageProducer"/> </fulfill> </instance> </fulfill>
AuditMessagePayload
All message producers should create audit messages in the same way, so that a UI implementation that is responsible for showing such messages does not need to make many case distinctions. To achieve this, the type AuditMessagePayload
has been introduced that can hold the information that message producers can deliver and the UI code can use for the presentation of such messages.
The AuditMessagePayload
holds:
AbstractPayloadAuditMessageProducer
The AbstractPayloadAuditMessageProducer
should be the base class for all ORM auditor message producers that create audit messages with messages of type AuditMessagePayload
.
It provides an implementation for the createAuditMessage()
method and handles many common cases that can be found in the persistence model of the Intershop solution.
Main features of AbstractPayloadAuditMessageProducer
:
AuditMessage
getMainBusinessAuditedItems()
AuditMessagePayload
filled by several calls to methods implemented in the concrete subclassesThe description key should be named in the following way:
auditing.message.<ormActionType>.<PO class name>(.<parameter name>)*
e.g.:
auditing.message.CREATE.com.intershop.component.marketing.internal.rebate.PromotionPO.promotionID=Created promotion (id="{0}"). auditing.message.UPDATE.com.intershop.component.marketing.internal.rebate.PromotionPOAttributeValuePO.promotionID.avName.localeID=Updated promotion (id="{0}") custom attribute "{1}" (locale="{2}").
Within the Intershop solution, a common way to assign users and user groups to business objects is to create a hidden UserGroupPO
associated with the PO representing the actual business object. Auditing the UserGroupAssignmnetPO
and UserGroupUserAssignmentPO
directly would then result in messages that refer the user group objects the business user actually does not see in the UI. To overcome this, the bc_auditing provides two message producers.
<implementation name="ORMAuditor.UserGroupAssignmentAuditMessageProducer" class="com.intershop.component.auditing.internal.messageproducer.UserGroupAssignmentAuditMessageProducer" implements="com.intershop.component.auditing.capi.ormauditor.AuditMessageProducer"> <!-- parentUserGroupFilter - list of handlers for determining if an assignment to this parrent should result in an audit message --> <requires name="parentUserGroupFilter" contract="com.intershop.component.auditing.capi.messageproducer.UserGroupAuditMessageProducerTypeHandler" cardinality="0..n"/> <!-- typedUserGroupHandler - list of handlers for determining the BO represented by the user group PO, for parent and child relation of this assignment PO, a default handler is automatically added --> <requires name="typedUserGroupHandler" contract="com.intershop.component.auditing.capi.messageproducer.UserGroupAuditMessageProducerTypeHandler" cardinality="0..n"/> </implementation> <implementation name="ORMAuditor.UserGroupUserAssignmentAuditMessageProducer" class="com.intershop.component.auditing.internal.messageproducer.UserGroupUserAssignmentAuditMessageProducer" implements="com.intershop.component.auditing.capi.ormauditor.AuditMessageProducer"> <!-- typedUserGroupHandler - list of handlers for determining the BO represented by the user group PO, for parent this assignment PO (child is always the user), also used for filtering --> <requires name="typedUserGroupHandler" contract="com.intershop.component.auditing.capi.messageproducer.UserGroupAuditMessageProducerTypeHandler" cardinality="0..n"/> </implementation>
Those two message producers can be customized by filling their requirements with UserGroupAuditMessageProducerTypeHandler
that perform mapping of a user group to the business object it is associated with. A default handler "ORMAuditor.GenericUserGroupHandler"
is provided. It maps the user group to itself as its BO.
Produced description keys:
#auditing.message.CREATE.com.intershop.beehive.core.internal.user.UserGroupAssignmentPO.<parent type>ID.<child type>ID=Added <child type> (id="{1}") to <parent type> (id="{0}"). #auditing.message.DELETE.com.intershop.beehive.core.internal.user.UserGroupAssignmentPO.<parent type>ID.<child type>ID=Removed <child type> (id="{1}") from <parent type> (id="{0}"). auditing.message.CREATE.com.intershop.beehive.core.internal.user.UserGroupAssignmentPO.usergroupID.usergroupID=Added user group (id="{1}") to user group (id="{0}"). auditing.message.DELETE.com.intershop.beehive.core.internal.user.UserGroupAssignmentPO.usergroupID.usergroupID=Removed user group (id="{1}") from user group (id="{0}"). #auditing.message.CREATE.com.intershop.beehive.core.internal.user.UserGroupUserAssignmentPO.<parent type>ID.userLoginName=Added user (id="{1}") to <parent type> (id="{0}"). #auditing.message.DELETE.com.intershop.beehive.core.internal.user.UserGroupUserAssignmentPO.<parent type>ID.userLoginName=Removed user (id="{1}") from <parent type> (id="{0}"). auditing.message.CREATE.com.intershop.beehive.core.internal.user.UserGroupUserAssignmentPO.usergroupID.userLoginName=Added user (id="{1}") to user group (id="{0}"). auditing.message.DELETE.com.intershop.beehive.core.internal.user.UserGroupUserAssignmentPO.usergroupID.userLoginName=Removed user (id="{1}") from user group (id="{0}").
Attributes of the payload attribute changes are named parent<parent type>Ref
or child<parent type>Ref
.
The ORMAuditTarget
is responsible for receiving an AuditEvent
and writing it to the database. The database table is audititem
. All information is inserted to the database by using JDBC. The ORM layer is not used because no events should be distributed and the audit item information will not be changed.
ORMAuditTarget
uses the ORM transaction if any is active. If the transaction gets rolled back, no audit event is stored either.ORMAuditTarget
opens its own ORM transaction.isORMBoundTransaction()
flag. If set to false
and an ORM transaction is present, the audit item is inserted within its own transaction.ORMAuditTarget
disables the CSRF-check()
while writing audit events into the database. This allows to audit user operations that are protected against CSRF attacks.AuditEvent field | Database field | Comment |
---|---|---|
getEventCreationTime | eventDate | pkey; a date |
getAuditTransactionID | transactionID | pke; a UUID for all objects in this audit transaction |
getEventNumber | eventID | pke; number counter for audit event, start with 0 |
- | itemID | pkey;counter for Objects, start with 0 |
getTransactionStartTime | transactionStartDate | a date; Is the time the audit transaction starts. |
getEventType | eventtype | position in enum; BEGIN(1), UPDATE(2), ... |
getApplication | applicationRef | will be the ref string of the corresponding |
auditEvent.getAuditMessage().getObjects() | domainRef | for each object the source domain reference; serialized with |
getUser | userRef | AuditUserRef |
getUser | userInfo | first name, last name and email from default address book |
auditEvent.getAuditMessage().getObjects() | objectType | reference object of the PO; e.g., full class name of |
auditEvent.getAuditMessage().getObjects() | objectRef | reference string of object; e.g., |
auditEvent.getAuditMessage().getAction() | actionType | if object was created, updated... |
auditEvent.getAuditMessage().getMessage().getClass() | payloadType | type of the payload; e.g., |
auditEvent.getAuditMessage().getMessage() | payload | a JSON-encoded string of changes; { "attributeChanges" : [ { "name" : "enabledFlag", "newValue" : true, "oldValue" : false } ], "attributeDataType" : "com.intershop.component.marketing.internal.rebate.PromotionPO", "descriptionKey" : "auditing.message.UPDATE.com.intershop.component.marketing.internal.rebate.PromotionPO.promotionID", "descriptionParameters" : [ "ftsKAM40PfQAAAE40QEDrA80" ], "metaInformation" : { "promotionRef" : { "@class" : "com.intershop.component.marketing.internal.promotion.auditing.refs.AuditPromotionRef", "domainRef" : { "@class" : "com.intershop.component.auditing.capi.ref.objects.AuditDomainRef", "domainName" : "PrimeTech-PrimeTechSpecials" }, "promotionID" : "ftsKAM40PfQAAAE40QEDrA80" } } } |
To work with a single audit item, do the following:
AuditItemBORepository auditItemBORep = applicationBO.getRepository("AuditItemBORepository"); AuditItemBO auditItemBO = auditItemBORep.getAuditItemBOByID(iD);
The AuditItemBORepository
is an extension of RepositoryBO
. From here you can get AuditItemBORepository
. Its only method is getAuditItemBOByID
. The ID parameter is a combined key of eventDate,transactionID,eventID,itemID
. They are separated by comma.
To fetch many audit items, you can use the AuditItemSearch.query
query.
The AuditItemBO
represents a single audit item record from the database. It provides methods for presentation.
AuditApplicationRef getApplicationRef(); AuditDomainRef getDomainRef(); AuditUserRef getUserRef(); Date getCreationDate(); String getObjectType(); String getActionType(); String getAuditMessage(); Set<AuditedItemAttributeChange> getAttributeChanges();
The getAttributeChanges
method contains a list of attribute changes. Each change has a name, type, old and new value.
The display extension wraps the AuditItemBO
and adds extra localization functionality. Attribute changes are displayed using a user-friendly localized string instead of the raw attribute names coming from the system. The location of the persistent object determines the location of the localization file (auditing_en.properties). E.g., for PromotionPO
, it is bc_marketing\staticfiles\cartridge\localizations\auditing_en.properties.
An entry in the localization file looks like this:
auditing.message.CREATE.com.intershop.component.marketing.internal.rebate.PromotionPO.promotionID=Created promotion (id="{0}").
This localizes a create action type for object com.intershop.component.marketing.internal.rebate.PromotionPO
and key promotionID
. The tangible keys and their value parameters are defined in the corresponding message producers.
The example:
auditing.attribute.com.intershop.component.marketing.internal.rebate.PromotionPO.enabledFlag.displayname=is enabled
localizes an attribute for objects of type com.intershop.component.marketing.internal.rebate.PromotionPO
with the key enabledFlag
.
In addition to mapping attribute names, the localization file can also be used to map technical attribute values to human readable ones. In the following example, a numeric value is mapped to a string readable by the user:
auditing.attribute.com.intershop.component.marketing.internal.rebate.PromotionPO.enabledFlag.value.0=yes auditing.attribute.com.intershop.component.marketing.internal.rebate.PromotionPO.enabledFlag.value.1=no
With this entry, the display extension uses yes
or no
instead of 0
or 1
.
Readability can also be improved by using a formatter for a attribute value:
auditing.attribute.com.intershop.component.marketing.internal.promotion.condition.RebateConditionPOAttributeValuePO.ConditionIntraDayEndTime.value={0,time}
The above entry causes the framework to output only the time from a Java date object.
The general form of a mapped entry is:
auditing.attribute.<PO class name>.<attribute name>.value.<attribute value>=<mapped value>
or
auditing.attribute.<PO class name>.<attribute name>.value=<formatter specification>
Different audit targets can be configured at: bc_auditing_orm\staticfiles\cartridge\components\instances.component
<fulfill requirement="target" of="AuditEngine"> <instance name="AuditTarget.ORMTarget" with="AuditTarget.ORMTarget"/> </fulfill>
An audit target having a filter can be configured as described in: section Auditing Engine section: Targets and Filters
At the user interface, the current implementation allows to select channels the audit objects are in. Data belonging to a channel may be in different domains. Currently, the RepositoryDomain
or the OrganizationDomain
are taken. The mapping between channels and domains is done by the pipeline: ProcessAuditReportGetDomains-GetDomains
. Overload the pipeline to add more domains.
Object types are configured at: sld_ch_consumer_plugin\staticfiles\cartridge\config\auditing.properties
# Promotions com.intershop.auditing.report.objecttype.Promotion=com.intershop.component.marketing.internal.promotion.auditing.refs.AuditPromotionCodeGroupRef, \ com.intershop.component.marketing.internal.promotion.auditing.refs.AuditPromotionRef, \ NamedObject:com.intershop.component.auditing.capi.ref.objects.AuditJobConfigurationRef:PromotionImpex-Export, \ VirtualObject:DirectoryEvent_PromotionAttachment, \ VirtualObject:FileEvent_PromotionAttachment com.intershop.auditing.report.objecttype.Product=com.intershop.beehive.xcs.capi.auditing.refs.AuditProductRef, \ com.intershop.beehive.xcs.capi.auditing.refs.AuditDerivedProductRef, \ com.intershop.beehive.xcs.capi.auditing.refs.AuditProductViewRef, \ com.intershop.component.shipping.capi.auditing.refs.AuditProductShippingSurchargeRef, \ com.intershop.component.image.capi.auditing.refs.AuditImageRef, \ NamedObject:com.intershop.component.auditing.capi.ref.objects.AuditJobConfigurationRef:ProcessBatchJob-Start@Catalog, \ NamedObject:com.intershop.component.auditing.capi.ref.objects.AuditJobConfigurationRef:ProcessBatchJob-Start@SearchIndexGenerationproduct*
In this example, AuditPromotionRef
belongs to the object type: Promotion. At the user interface, the system allows the user at the back office to select Promotion. The class AuditItemObjectTypeMapper
translates it for the search query amongst others to com.intershop.component.marketing.internal.promotion.auditing.refs.AuditPromotionRef
.
The values are strings that are used for the search. If the value ends with a *
the search finds all entries starting with this value. Please see column objecttype of table audititem.
The object type is part of an AuditEvent
. In an AuditTarget
class, the object type is created from AuditObjectRef
by using a deserializer.
ObjectSerializer
can be configured by the configuration framework. Have a look at bc_auditing...instances.component -> AuditItemSerializerRegistry
.
Here, three types of audit reference types are defined:
ClassNameSerializer
: a ref class of type AuditObjectRef
. This class represents a real PO object.NamedAuditObjectRefTypeSerializer
: e.g., a job configuration is not a object type on its own, instead a job is made for doing something with other business objects. The NamedAuditObject
can be used to create a custom "objecttype" that not only consists of the Java data type but also from values.VirtualAuditObjectRefTypeSerializer
: If there is no (good) representative (Java) object related to the operation, a VirtualObject
can be used for the audit message; in this example, it is used to record file system changes.SLD_VIEW_AUDIT_REPORTS
. The enterprise is added as first element (CurrentOrganization).com.intershop.auditing.report.objecttype.*
. Currently, the file sld_ch_consumer_plugin\staticfiles\cartridge\config\auditing.properties is used. The values there get localized with auditing.report.objecttype.
.com.intershop.auditing.report.actiontypes
. They are localized with: auditing.actiontype.*
.The main aspect of serialization in the auditing context is to write and read a certain object state in and from the database. The way how to serialize and deserialize the object should depend on the implementation of the object but should not be determined in the source code. That means, one can define a number of object serializers for one class, but which one is used depends on the configuration of the system.
The idea is to have specialized serializers which are capable of serializing an object into a special representation and to deserialize this object from exactly this representation. It is important to use the same serializer for both directions; otherwise, it cannot be guaranteed that the object is deserialized into its original state.
The object serializer uses a class (name) to decide if it is able to serialize/deserialize the given object/string. This class can be an interface/supertype of the actual object. So one might want to write a persistent object serializer which is able to serialize all persistent objects (like ProductPO
, DomainPO
, JobConfigurationPO
, ..) by their UUID as a "catch all backup" (which indeed is not very useful). However, after that you can create some specialized serializers for ProductPO
and DomainPO
that are responsible for other issues (and register them with a higher priority - see below).
The interface is kept simple:
public interface ObjectSerializer<T> { String serialize(T toBeSerialized) throws IOException; <U extends T> U deserialize(String toBeDeserialized, Class<U> type) throws IOException; boolean isClassSupported(Class<?> clazz); }
The registry is responsible for keeping all available serializers in a central place.
As it can be necessary to serialize/deserialize objects for different scopes/purposes, the registry works purpose-based. That means, for one class that should be serialized you can have different serializers for different purposes.
For example, you want to serialize a product into XML and JSON, so you need to have two serialization providers, one for XML and one for JSON. Both will be registered at the registry, and you can access both for different purposes.
Remember that a serialization provider should be capable to define interfaces/supertypes as its supported class. So it may be possible that for one purpose and one class, more than one serialization provider is registered.
For example (see above), you have a serializer that is able to serialize PersistentObjects
(by their UUID) and some other serializer that does some more detailed serialization for ProductPO
, DomainPO
and so on. You register all these at the registry. If you ask the registry for a serializer for ProductPO
, at least two serializers will be returned - the special one and another one for all PersistentObjects
. But which one should we use?
To solve this problem, the current implementation of the registry uses a priority for each serialization provider. The priority is not exposed by the interface, but the implementation returns a priority-ordered collection (first element of the collection is the element with the highest priority) or the element with the highest priority.
You can replace the current implementation of the registry with your own if you need another algorithm of sorting the providers.
public interface ObjectSerializerRegistry { <T> ObjectSerializer<T> getSerializer(String purpose, Class<? extends T> type); <T> Collection<ObjectSerializer<T>> getSerializers(String purpose, Class<? extends T> type); }
Currently, there is one global serialization registry instantiated using the component framework. This registry is used to register any available serialization provider, so there is a central place to find them. The providers will have a priority and will be registered for their purposes.
For this purpose assignment, a helper class is used which is not exposed by the interface of the serialization provider registry.
Here is a sample registration:
<instance name="AuditItemSerializerRegistry" with="AuditItemSerializerRegistry" scope="global"> <fulfill requirement="serializerPurposeAssignment"> <instance with="AuditItemSerializerAssignment" name="MessageAuditItemSerializers"> <fulfill requirement="purpose" value="message" /> <fulfill requirement="objectSerializer" > <instance with="audit.message.AuditMessagePayloadJSONSerializer" scope="global"> <fulfill requirement="priority" value="100" /> </instance> <instance with="audit.message.StringSerializer" scope="global"> <fulfill requirement="priority" value="100" /> </instance> </fulfill> </instance> </fulfill> </instance>
The code shows an AuditItemSerializerRegistry
instance which fulfills its requirement serializerPurposeAssignment
with one assignment object. This is the mentioned helper class. This assignment gets the purpose "message", and two serialization providers with the same priority are added to the assignment. The priorities of both providers do not differ as both might support different classes. If both serializers support the same class, the priority must differ.
Assume you have an object of type T
and want to serialize this into a string - and you want to deserialize it into an object with your type T
. What you need to know is the purpose for which your serialization should take place.
<instance name="MyComponentInstance" with="MyComponentInstanceImpl" scope="global"> <fulfill requirement="serializerRegistry" with="AuditItemSerializerRegistry" /> </instance>
ComponentMgr compMgr = NamingMgr.getManager(ComponentMgr.class); ObjectSerializerRegistry serializerRegistry = compMgr.getGlobalComponentInstance("AuditItemSerializerRegistry");
T anObject = ... Class<T> anObjectClass = anObject.getClass(); String aSerializedString = null; ObjectSerializer<T> serializer = (ObjectSerializer<T>)serializerRegistry.getSerializer("purpose", anObjectClass); if (serializer != null) { try { aSerializedString = serializer.serialize(anObject); } catch(IOException e) { // do something with that exception } }
String aSerializedString = ... Class<T> anObjectClass = ... T anObject = null; ObjectSerializer<T> serializer = (ObjectSerializer<T>)serializerRegistry.getSerializer("purpose", anObjectClass); if (serializer != null) { try { anObject = serializer.deserialize(aSerializedString, anObjectClass); } catch(IOException e) { // do something with that exception } }
There are currently two different purposes for object serializers:
AuditObjectRefSerializer
which uses the AuditObjectRef
interface (see below). For deserialization, the type of the serialized objects can be obtained from OBJECTTYPE.Auditing data can be stored for a long period of time, even longer than products, categories, promotions etc. will exist in the system. Hence, we cannot "hard" reference on these objects, but we need some "weak" references to these objects. References can in the best scenario restore the original object (if it is still available in the data base) but in the worst scenario provide as much human readable information to identify which object has been changed by whom. So the idea is to have an object reference or a so-called ObjectRef
.
The ObjectRef
should consist of "simple" Java types (like String
, int
, float
, ...) or of other ObjectRefs
, at least of any type that can be easily serialized and stored. But to keep it less error-prone, only "simple" types should be used.
Samples of ObjectRefs
are:
AuditObjectRef
Since ObjectRefs
are usually used to be stored as a serialized string in the database, there has to be an easy way to do so. For this, the AuditObjectRef
interface was created which defines one method: getRefString() : String
. If an object reference implementation implements this interface and in this way implements the getRefString()
function, it should also provide a one string constructor which will be used for deserialization.
The object reference serializer follows the serializer concept (see above). As its supported class it uses the AuditObjectRef
interface. During serialization it uses AuditObjectRef.getRefString()
and for deserialization it tries to find the string constructor from the provided type, which will be called with the serialized string. So it is important that any object reference which implements the AuditObjectRef
interface also provides a string constructor.
In some circumstances, you may not know what the current type of the object is, for which you need to create an object reference. For these situations, the object reference provider and the object reference provider registry were created.
An object reference provider knows which class it supports. This can be asked by isClassSupported
. This function checks if the given class is assignable from the class that the provider supports. In other words, the provider also supports supertypes and interfaces; and it can be possible that there is more than one provider for one type. If a class is supported, getObjectRef
will return an object reference for the given object.
public interface AuditObjectRefProvider<T> { boolean isClassSupported(Class<?> clazz); AuditObjectRef<? extends Object> getObjectRef(T o); }
The registry is a global component where all available object reference providers need to be registered and where they can be accessed. The registry interface offers two methods:
public interface AuditObjectRefProviderRegistry { <T> AuditObjectRefProvider<T> getObjectRefProvider(String pupose, Class<? extends T> type); <T> Collection<AuditObjectRefProvider<T>> getObjectRefProviders(String purpose, Class<? extends T> type); }
"purpose" defines a special scope an object reference provider is registered for. For the same type but for different scopes different object reference providers can be returned, which serve different object references.
As for one purpose and for one type different object reference providers can be returned, looking at the interface it is not clear which one will be returned. To decide this, the current implementation of the AuditObjectRefProviderRegistry
works at a priority level. That means, at registration time every provider gets a priority and the registry returns the provider with the highest priority.
The only instance of the object reference provider registry is instantiated using the component framework. Every instance of an object reference provider is wired at the registry and can be accessed there. Here is an example:
<instance name="AuditObjectRefProviderRegistry" with="AuditObjectRefProviderRegistry" scope="global"> <fulfill requirement="objectRefProviderPurposeAssignment"> <instance with="AuditObjectRefProviderAssignment" name="ReferenceObjectRefProviders"> <fulfill requirement="purpose" value="reference" /> <fulfill requirement="objectRefProvider" > <instance with="audit.reference.ApplicationRefProvider" scope="global"> <fulfill requirement="priority" value="100" /> </instance> <instance with="audit.reference.DomainRefProvider" scope="global"> <fulfill requirement="priority" value="100" /> </instance> </fulfill> </instance> </fulfill> </instance>
An assignment helper class is used to model the purpose-based assignments between the registry and the providers. The assignment gets the purpose "reference", and two object reference providers assigned. Both have the same priority as they both support different types.
One has an object of type T
and wants to get an object reference for this object for the purpose "purpose
".
<instance name="MyComponentInstance" with="MyComponentInstanceImpl" scope="global"> <fulfill requirement="refProviderRegistry" with="AuditObjectRefProviderRegistry" /> </instance>
ComponentMgr compMgr = NamingMgr.getManager(ComponentMgr.class); AuditObjectRefProviderRegistry objectRefProviderRegistry = compMgr.getGlobalComponentInstance("AuditObjectRefProviderRegistry");
If you have the registry, you can access the object reference provider and create an object reference.
T object = ... Class<T> objectClass = object.getClass(); AuditObjectRef<? extends Object> objectRef = null; AuditObjectRefProvider<Object> objectRefProvider = objectRefProviderRegistry.getObjectRefProvider("purpose", objectClass); if (objectObjectRefProvider != null) { objectRef = objectRefProvider.getObjectRef(object); }
Currently, there are two different types of purposes:
AuditObjectRef<Object>
for any object they get.AuditDomainRef
objects for all objects they get. This purpose was introduced to provide domain object references for (persistent) objects in a common way. Providers registered at this purpose should be able to return the proper domain reference of the given object. To get this domain reference, different approaches can be used: getDomain()
, getSite().getDomain()
, getRepositoryDomain()
, ... . Each provider for each object type should use the proper way.Due to the (growing) high number of different object references, it is hard to show their values localized in the auditing back office. A lot of instanceof checks and if-else constructions would be necessary; and if an object reference was added, all these code positions would have to be touched.
There is an easier approach to display the object references, but this relies on a handful of assumptions:
AuditObjectRef
AuditObjectRef
, the rule from above is usedFor these assumptions, a helper class has been created in sld_enterprise_app: com.intershop.sellside.enterprise.internal.auditing.AuditObjectRefDisplayKeyValueProvider
This class does the following:
Map<String, Object>
audit.objectref.<class name>.<function name>
There is also a localizations/auditing_en.properties
file which contains all currently existing object reference classes with their functions and a localized text. The text of these localization keys should have (only one) placeholder defined which will get the actual value of the getter (coming from the map). E.g.:
auditing.objectref.com.intershop.component.auditing.capi.ref.objects.AuditApplicationRef.urlid=URL identifier: {0} auditing.objectref.com.intershop.component.auditing.capi.ref.objects.AuditApplicationRef.sitename=Site name: {0}