7.8:dev:audit

Auditing

TODO: Introduction

Auditing of entities is done by:

  • Hibernate envers - Historizes data. Audit logs are saved in tables with _a suffix according to a specific entity. Auditing is linked to all the basic entities, except for entities which cannot be audited. (IdmAudit, IdmPassword, etc.).
  • IdmAuditListener - The individual tables are linked by the main auditing entity = IdmAudit, over which an agenda is built which can be searched via all audited entities. IdmAuditListener handles filling in of this agenda. It listens to changes over all the entities being audited and creates references in tables with historical data created via hibernate envers.
  • AuditableEntityListener - handles filling in of the audit attributes in entities (date of creation, modification, etc.).
  • AuditableInterceptor - ensures that the once filled-in date of creation is not overwritten when the log is modified.
IdmAudit is the only entity in the CzechIdMng system which has Long as its primary key. All the other entities have UUID. This fact is unchangeable and needed by Envers.

New entity, how to enable auditing

There is a simple way of "enabling" versing for an entity in Hibernate envers. It is done simply by writing the annotation @Audited before the entity's definition. We DO NOT USE this way in the CzechIdMng system. Instead, we write this annotation to the individual entity attributes which we want to audit. For example:

 @Audited
 @NotNull
 @Column(name = "disabled", nullable = false)
 private boolean disabled = false;

IdmAuditService serves as a mediator between Hibernate envers and IdmAudit. The service provides an access to the audit metadata (IdmAudit) and the versions themselves. The basic methods of this service are divided into findRevision and findVersion. The description of their differences can be found below.

Revisions in the CzechIdMng system are basic metadata about the individual revisions. The metadata is propagated to the front-end to the agenda itself. Using revisions, we are able to ascertain the following metadata:

  • entityId (UUID of the entity involved in the revision)
  • modification (type of modification, adding, removing, modification)
  • type (type of the audited entity, class name)
  • changedAttributes (a chain which is, in case of modification, filled in with the names of the columns which have been changed)
  • modifier (username of the identity, which has made a change, adding or removing)
  • modifierId (UUID of the identity, which has made a change, adding or removing)

The findRevision methods are able to return exactly one revision for the given class, entity ID and revision ID, and also all the revisions for the class and entity ID. Using the getPreviousRevision method, it is possible to get the previous revision according to the ID of the revision.

A version in the CzechIdMng system is a snapshot of the entity at a given time. Methods handling versions do not allow getting all the versions for the entity. Instead, they support getting one version getVersion, the previous version getPreviousVersion, and the difference (diff) between the versions getDiffBetweenVersion. If we need to ascertain the last entity version, we can use the getLastVersionNumber method.

If we do not know the exact entity type (class), an object will be returned as the version. If we want to get the individual values from this unidentifiable type, we can use the getValuesFromVersion method. If we need a list of the changed attributes between the versions, we can use the getDiffBetweenVersion method.

A complete list of the audited entities can be acquired by using the getAllAuditedEntitiesNames method.

The current version of these methods can be found in the IdmAuditService interface. The most important methods have been chosen.

<T> T findRevision(Class<T> classType, UUID entityId, Long revisionId)

The method returns one revision, if it exists, or it returns an exception RevisionDoesNotExistException

<T> List<IdmAudit> findRevisions(Class<T> classType, UUID entityId)

The method returns all revisions for the entity. If no revision is found, an empty list is returned.

<T> T getPreviousVersion(T entity, Long currentRevId)

It returns the exact type of version, to the previous revision. The previous version is established using the revision ID.

Object getPreviousVersion(Long currentRevId)

It returns the previous entity version for the id revision. The version type cannot be determined easily at this moment.

<T> T getPreviousVersion(Class<T> entityClass, UUID entityId, Long currentRevId)

The method returns the exact version type which is determined by revision ID and entity ID.

<T> Number getLastRevizionNumber(Class<T> entityClass, UUID entityId)

The method returns the last available revision number.

<T> List<String> getNameChangedColumns(Class<T> entityClass, UUID entityId, Long currentRevId, T currentEntity)

It returns a list of changed entity attributes. Note that only attributes with the @Audited annotation are accepted.

List<String> getAllAuditedEntitiesNames()

A list of all entities containing at least one @Audited annotation is returned.

Map<String, Object> getDiffBetweenVersion(String clazz, Long firstRevId, Long secondRevId)

A map is returned, where the key is the name of the attribute which has been changed in the second revision. The value is then the changed value from the second version.

Map<String, Object> getValuesFromVersion(Object revisionObject)

If we do not know the exact version type, we can get it using PropertyUtils.getPropertyDescriptor(getPropertyDescriptor)

The information about audit is located on the /audits endpoint. The right for endpoints AUDIT_WRITE does not exist for audit. All the other information about audit are secured using AUDIT_READ. If a user owns a role with this permission, he is able, for example, to display data about users, to which he does not have to have any rights at all. This situation is desired.

Apart from the basic endpoints, the REST controller is extended by these endpoints.

The previous revisions is returned to the revision ID. If it does not exist, http status 404 is returned.

The endpoint compares the difference between two versions of revisions.

The same detail has been created for all entities to display audit detail. This detail enables displaying and comparison of two revisions including the difference of their version values.

The listener IdmAuditListener handles filling in of revision metadata.

At the moment, it performs these operations:

  • Sets the attributes: type (class name), modification, modifier name and ID, audited entity ID.

If it is the MOD (modification) type modification, comparison with the previous revision is done and the attribute changedColumns is given the names of the changed attributes.

During synchronization (and not only there), a problem has been found with multiple saving of one entity in one transaction. For Envers, one transaction means one revision (the revision can then contain multiple versions/snapshots of various entities. If a creation of an entity is done in a transaction and then, after saving it, it is updated, the revision will contain two versions/snapshots of the entity. The problem comes when we want to fill in the list of changed attributes in the version because the previous version has not been committed yet.

Temporary solution: The functionality of changed attributes is temporarily disabled, the list of changed attributes can only be seen in the detail.

  • Interconnecting with the planned operations audit (linking entities to transaction, from clicking the button on the FE to saving in the database)
  • adding information about the current PPV in the detail of identity audit
  • adding the stress test

Audit for identities

To make operating with the audit for identities better (for example filtering by username), a FE agenda was created for searching identities with their relationship. 6 new columns were added for the entity IdmAudit that are filled in the IdmAuditListener. These columns are filled only when the audited entity implements the interface AuditSearchable (for example IdmIdentityRole).

Columns description

  • ownerId - unique identifier of the owner of this entity,
  • ownerCode - human readable identifier of the owner of this entity (example username),
  • ownerType - class of the owner,
  • subOwnerId - unique identifier of the owner of this entity,
  • subOwnerCode - human readable identifier of the owner of this entity,
  • subOwnerType - class of the sub owner.

The use of Sub owner columns is recommended only for intersection tables. The example of filling the attributes (IdmIdentityRole):

@Override
public String getOwnerId() {
	return this.getIdentityContract().getIdentity().getId().toString();
}
 
@Override
public String getOwnerCode() {
	return this.getIdentityContract().getIdentity().getCode();
}
 
@Override
public String getOwnerType() {
	return IdmIdentity.class.getName();
}
 
@Override
public String getSubOwnerId() {
	return this.getRole().getId().toString();
}
 
@Override
public String getSubOwnerCode() {
	return this.getRole().getCode();
}
 
@Override
public String getSubOwnerType() {
	return IdmRole.class.getName();
}

Endpoint for getting some entity with their relation is /audit/search/entity (IdmAuditController). The parameter entity is required, the value is the name of the class. For this class the implementation of AbstractAuditEntityService is found (service method DefaultAuditService (findEntityWithRelation))