Concept - Auditing

Introduction


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.

In General

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:

  • Consistency between data within the system and the audit trail
    • This implies that if a business operation fails, the audit trail must not contain an entry for a success; also, if auditing fails, the business operation must be rolled back. This leads to the concept of audit transaction.
  • Configurability of the actual target for the audit messages
    • In any case, it must be possible to route messages to an external system. (This is also a requirement for PA-DSS certification).
  • High customizability for most features
    • Since auditing brings an extra overhead to the business functionality, this allows to reach the maximum performance for each project by omitting as much as possible.

References

Wikipedia: Audit Trail

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).

Overview

The diagram shows the main components involved in the processing of audit messages, which are:

  • Custom / Application code that is enriched with statements for submitting audit messages
  • An ORM transaction listener that can produce audit messages for any persistent object without a need for application code changes
  • The core framework that brings connection between application code and the audit targets
  • Audit targets/appender that manage the storage, or sends messages to external systems, incl. required transformations
  • A BO layer that forms the base the UI audit browser works on

Audit Engine

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.

Auditing Transactions

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:

  • Operations to be audited
    • Auditing of transactional secured business operations, e.g., property changes of business objects stored within a database
    • Auditing of business operations not secured by any database transaction
  • The target for audit messages
    • Writing audit events into the same transactional context as the business operation to be audited
    • Writing without any transactional context or a different one to the business operation

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:

  • Allows the AuditEngine to return an optimized implementation depending on the featured actual configured audit targets
  • Allows compact and safe code by using the AutoCloseable feature of Java 7
  • Supports distributed and nested business code resulting in many audit log messages belonging to the same audit transaction
  • Hides the dependencies to the actual ORM transaction so that the audit transaction can be correctly marked if the ORM transaction is committed or rolled back/failed

The 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

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.

  • The name of a pipeline marker has the form: pipeline.<cartridge>.<pipeline>.<start node>.
  • It is not required to declare them in the component files explicitly unless explicit marker relations are to be created.
  • The marker registry arranges the pipeline markers automatically in a hierarchy.

This example means that the marker:

  • pipeline.sld_ch_base.ViewPromotionAttachmentUpload.AddDirectory is included in pipeline..ViewPromotionAttachmentUpload.AddDirectory* and pipeline.sld_ch_base.ViewPromotionAttachmentUpload.*
  • pipeline..ViewPromotionAttachmentUpload.AddDirectory* is included in pipeline..ViewPromotionAttachmentUpload.**
  • pipeline.sld_ch_base.ViewPromotionAttachmentUpload.* is included in pipeline..ViewPromotionAttachmentUpload.* and *pipeline.sld_ch_base..**
  • pipeline...* includes pipeline..ViewPromotionAttachmentUpload.* and *pipeline.sld_ch_base..**

Audit Messages / Events

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:

  • Action: An identifier for the kind of action that is performed, e.g., CREATE, UPDATE, DELETE, LOGIN, and LOGOUT. There is no predefined set of such identifiers, the actually used values depend on the business context and on how messages are searched later on.
  • Objects: The main participants related involved in the operation to be audited. Typically, it is the object that has been created, updated or deleted or the business object that can be seen as the owner of the actual changed implementing object. But if a relation between objects is created, the audit messages should refer to the business objects that the relation connects instead of a representative of the relation itself. For instance, this is the case for product-category-assignments where the assignment can be seen as a property of the category or the product.
  • Message: The actual message. It can be of any Java object type that can be handled by the audit targets. A plain string should always work, but the framework also provides the AuditMessagePayload.
  • Origin: A string that like the logging category should make the code place for the actual message identifiable. This helps investigating issues with bad messages especially during development.

Note

Audit messages should be as self-explanatory as possible, since they can live longer than the business objects involved.

The AuditEvent contains:

  • Audit message: the same object as submitted by the business code. It is null for the automatically created transaction events for the start and the end of audit transactions.
  • Transaction information: The transaction ID, transaction start time, the event number within the transaction, the transaction state (BEGIN, UPDATE, COMMIT, ROLLBACK, FAIL), markers, and kind of transaction (ORM-bound or not).
  • User: the user responsible for the business action to be audited. Actually, it is taken from the current request, if any; otherwise it is an anonymous user.
  • Application: the application in which context the business action occurred.
  • Event creation time: The time the event was created.

Targets

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.

Filters

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.

Configuration

Enabling the Auditing Feature

<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. 

Audit Engine

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");

Markers

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.

Targets and Filters

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 via ORM Listening

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.

ORMAuditor

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:

  • Can listen to all ORM transactions
  • Audit events can be restricted by filtering
    • For the execution context, e.g., if the current thread contains a valid request (see com.intershop.component.auditing.capi.ormauditor.ContextFilter)
    • For the current application context
    • For object types (attributes) per application context

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.

Message Producer

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 and AbstractPayloadAuditMessageProducer

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:

  • A description key that is a localization key under which the UI can find the fully localized human readable message for the audit event.
  • The description parameters which hold the values for the parameter placeholders referred by the message format associated with the description key.
  • A list of attribute changes where one change is the sum of attribute name, old value and new value and optionally the locale.
  • The attribute data type which is the name of the data type that (virtually) holds the attributes collected in the list of attribute changes. This allows building unique keys to localize attribute names within the UI.
  • A map of additional meta information. This is currently not used within the UI.

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:

  • Constructing and filling the AuditMessage
    • Origin - set to the actual current class name
    • Action - set to the performed ORM operation CREATE, UPDATE OR DELETE
    • Objects - set to the result of the abstract method getMainBusinessAuditedItems()
    • Message - the AuditMessagePayload filled by several calls to methods implemented in the concrete subclasses
  • Handling of custom attributes of ExtensibleObject
  • Support for combined attribute, e.g., money, stored with 2 columns in the database, but only one attribute change should be generated that holds real money objects
  • Support for creating payload attribute changes
  • Support for building description keys in a consistent form

The 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}").

Auditing Assignments to (Hidden) User Groups

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.

ORM Target / Audit Item Repository

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.

Handling of Data

  • ORMAuditTarget uses the ORM transaction if any is active. If the transaction gets rolled back, no audit event is stored either.
  • If no ORM transaction is available, ORMAuditTarget opens its own ORM transaction.
  • The AuditEvent contains an isORMBoundTransaction() flag. If set to false and an ORM transaction is present, the audit item is inserted within its own transaction.
    This ensures that if the ORM transaction is rolled back, the audit item is written to the database.
  • The ORMAuditTarget disables the CSRF-check() while writing audit events into the database. This allows to audit user operations that are protected against CSRF attacks.

Mapping From AuditEvent Into Database

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 AuditApplicationRef

auditEvent.getAuditMessage().getObjects()

domainRef

for each object the source domain reference; serialized with ObjectSerializer<AuditObjectRef>

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 AuditPromotionRef

auditEvent.getAuditMessage().getObjects()

objectRef

reference string of object; e.g., ftsKAM40PfQAAAE40QEDrA80@PrimeTech-PrimeTechSpecials

auditEvent.getAuditMessage().getAction()

actionType

if object was created, updated...

auditEvent.getAuditMessage().getMessage().getClass()

payloadType

type of the payload; e.g., com.intershop.component.auditing.capi.payload.AuditMessagePayload

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"
			}
		}
}

Audit Item Repository

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.

AuditItemBO

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.

AuditItemBODisplayExtension

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>

Configuration of AuditORMTarget

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

Search Correct Domains

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.

Search Object Types

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.

AuditItem Back Office User Interface

  • The select box Channels is provided by filtering all channels with the right: SLD_VIEW_AUDIT_REPORTS. The enterprise is added as first element (CurrentOrganization).
  • The select box User ID holds the list of all organization users.
  • The select box Object Type gets data from property 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..
  • The select box Action Type gets data from property com.intershop.auditing.report.actiontypes. They are localized with: auditing.actiontype.*.

Serialization and Deserialization

Object Serializer

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);
}

Serializer Registry

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);
}

Registration Using the Component Framework

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:

instances.component
<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.

Getting an Object Serializer Using the Component Framework

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.

accessing the registry via wiring
<instance name="MyComponentInstance" with="MyComponentInstanceImpl" scope="global">
    <fulfill requirement="serializerRegistry" with="AuditItemSerializerRegistry" />
</instance>
accessing the registry via name
ComponentMgr compMgr = NamingMgr.getManager(ComponentMgr.class);
ObjectSerializerRegistry serializerRegistry = compMgr.getGlobalComponentInstance("AuditItemSerializerRegistry");
serialize
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
    }
}
deserialize
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
    }
}

Purposes

There are currently two different purposes for object serializers:

  • message - This purpose is used for the database column PAYLOAD in table AUDITITEMPO. The Java type for objects serialized in this column is written into PAYLOADTYPE, so the class name for deserialization can be obtained from there.
  • reference - This purpose is used for serializing objects into the column OBJECTREF of table AUDITITEMPO. The objects serialized into this column will be obtained by specialized object reference providers (see below) and will be serialized also by an AuditObjectRefSerializer which uses the AuditObjectRef interface (see below). For deserialization, the type of the serialized objects can be obtained from OBJECTTYPE.

References

Object References

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:

  • DomainRef
    • domainName : String
  • ProductRef
    • domainRef : DomainRef
    • sku : String

Serialization and Deserialization

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.

Object Reference Serializer

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.

Object Reference Provider and Registry

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.

Registration Using the Component Framework

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:

instances.component
<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.

Getting an Object Reference Provider Using the Component Framework

One has an object of type T and wants to get an object reference for this object for the purpose "purpose".

accessing the registry via wiring
<instance name="MyComponentInstance" with="MyComponentInstanceImpl" scope="global">
    <fulfill requirement="refProviderRegistry" with="AuditObjectRefProviderRegistry" />
</instance>
accessing the registry
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.

create the 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);
}

Purposes

Currently, there are two different types of purposes:

  • reference - Object reference providers registered for this purpose should provide object references implementing AuditObjectRef<Object> for any object they get.
  • domain - Object reference providers registered for this purpose should provide 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.

Showing Auditing Object References Localized in the Back Office

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:

  • important attributes are accessible via a getter function
  • the getter has no arguments
  • the return types of these getters are string, primitive types and other AuditObjectRef
  • if the return type is AuditObjectRef, the rule from above is used

For these assumptions, a helper class has been created in sld_enterprise_app: com.intershop.sellside.enterprise.internal.auditing.AuditObjectRefDisplayKeyValueProvider

This class does the following:

  • it creates a map and returns it: Map<String, Object>
  • the values of the map are the return values of the getter functions
  • the keys of the map are calculated localization keys with the following format: 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_en.properties
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}
Disclaimer
The information provided in the Knowledge Base may not be applicable to all systems and situations. Intershop Communications will not be liable to any party for any direct or indirect damages resulting from the use of the Customer Support section of the Intershop Corporate Web site, including, without limitation, any lost profits, business interruption, loss of programs or other data on your information handling system.
Home
Knowledge Base
Product Releases
Log on to continue
This Knowledge Base document is reserved for registered customers.
Log on with your Intershop Entra ID to continue.
Write an email to supportadmin@intershop.de if you experience login issues,
or if you want to register as customer.