Table of Contents

Automatic Role Guarantor Transfer

When a role guarantor stops being able to act as a guarantor (the identity is deleted or blocked, or one of its contracts is deleted, deactivated or expires), the roles they guaranteed could be left without any active guarantor. This feature detects such situations and, before the destructive change is persisted, transfers the affected guarantees to a substitute (typically the original guarantor's manager / superior), so that every role keeps an active guarantor. Each new guarantor is then notified about the roles they became a guarantor of and why.

The transfer covers both kinds of role guarantee in CzechIdM:

Examples:

The feature is best-effort and non-blocking: a failure of the transfer (e.g. a misconfigured script) is logged but never aborts the original operation (delete / block / contract change / expiration de-provisioning).

Key concepts

Last (active) guarantor

A substitute is added only when removing the original guarantor would actually leave the role unguarded - i.e. the original guarantor is the last active one. (A direct guarantee is additionally always removed from an identity that becomes inactive, even when it was not the last guarantee - see Direct guarantees.) The "is there still someone else?" check considers only effective holders/guarantees:

Effectively active contract

A contract is considered effectively active when it has no special state (not EXCLUDED / DISABLED) and is valid now (validFrom/validTill). This is the same rule the exclusivity detection uses for "valid holder", and it drives the contract-level triggers.

Substitute

The identity (or identities) the guarantee is transferred to. Substitutes are resolved through a configurable system script (managers by default), with a robust fallback chain - see How a substitute is resolved.

When the transfer runs

The transfer always runs in the BEFORE phase of the triggering event (processor order CoreEvent.DEFAULT_ORDER - 100), so the live data needed to detect the last guarantor and to resolve managers still exists. Five event processors cover the situations in which a guarantor becomes inactive:

Trigger Event Processor Removes original holding? Reason code
Identity is deleted Identity DELETE IdentityGuaranteeTransferDeleteProcessor No - the delete cascade removes it IDENTITY_DELETED
Identity is manually blocked Identity UPDATEDISABLED_MANUALLY IdentityGuaranteeTransferDisableProcessor Yes - block does not remove role assignments IDENTITY_DISABLED
Active contract is deleted Contract DELETE IdentityContractGuaranteeTransferDeleteProcessor No - the delete cascade removes it CONTRACT_DELETED
Active contract is deactivated by an edit (validTill set to the past, validFrom set to the future, set EXCLUDED or DISABLED) Contract UPDATE (active → inactive) IdentityContractGuaranteeTransferDeactivateProcessor Yes - the contract stays in place CONTRACT_DEACTIVATED
Contract expires naturally (its validTill passes) Contract EXPIRED IdentityContractGuaranteeTransferExpiredProcessor No - the expiration task removes it CONTRACT_EXPIRED
For contract-level triggers, direct guarantees are handled only when the affected contract was the identity's last effectively active contract (i.e. the identity as a whole becomes inactive) - the identity is then removed from all roles it directly guarantees, with a substitute added where it was the last guarantee. Guarantee roles are always evaluated per the affected contract.
Besides these event processors, two nightly long-running tasks drive the same transfer where de-provisioning does not go through a contract event: HrEndContractProcess (natural date-expiry) and HrContractExclusionProcess (a contract set EXCLUDED outside the application, e.g. directly in the database) - see Natural contract expiry and the implementation reference.

What exactly is transferred

Direct guarantees

When the identity becomes inactive it is removed as a direct guarantee from every role it directly guarantees - an inactive identity must not remain a direct guarantor. This mirrors guarantee roles, which an inactive identity loses together with its role assignments.

For the subset of roles where the identity was the last active direct guarantee, a substitute is added first so the role keeps an active guarantor:

  1. the substitute(s) are added as a direct guarantee of the role (if not already a guarantee), then
  2. the original identity's direct guarantee is removed.

For roles that still have another active direct guarantee, the identity is simply removed - no replacement is needed.

Consistency rule: when the identity was the last guarantee but no substitute can be resolved, it is still removed. The role then ends up with no direct guarantee - the same outcome as a guarantee role left unfilled after its holder loses the role - rather than keeping an inactive guarantor artificially.

Guarantee roles

For every guarantee role exclusively held by the original identity (resp. through the affected contract):

  1. the guarantee role is assigned to the substitute(s) on the substitute's prime valid contract, via an internal role request executed immediately (without approval),
  2. by gaining the guarantee role, the substitute becomes a guarantor of all owner roles that use it,
  3. the original holding is removed when removeSource applies (see the table above) - direct holdings only; sub-roles are removed together with their direct role.

Substitutes for a guarantee role are resolved per source contract (the contract through which the original identity held the guarantee role) and cached, so the DB-heavy resolution runs once per source contract rather than once per guarantee role.

How a substitute is resolved

Substitutes are resolved by a robust fallback chain (RoleGuaranteeTransferManager.resolveSubstitutes):

  1. Configured system script - run the script configured by idm.sec.core.role.guarantee.transferScript (default roleGuaranteeTransferTargets), keep only the results that are IdmIdentityDto and active (state VALID, not disabled), and never the identity being removed. If this yields a non-empty list, those are the substitutes.
  2. Fallback role holders - otherwise (empty result, wrong-typed result or a script failure), use the active holders of the fallback role configured by idm.sec.core.role.guarantee.transferFallbackRole (the admin role when not configured).
  3. Admin identity - if there are still none, fall back to the single admin identity.

If not even the admin identity is available, the affected roles are left without an active guarantor and an error is logged.

Natural contract expiry

Contracts that elapse by date (their validTill passes without any user action) are de-provisioned by the nightly scheduled tasks. To let the transfer react before the contract's role assignments are removed, these tasks publish the EXPIRED event for each genuinely date-expired contract (state is null) right before removing its roles. IdentityContractGuaranteeTransferExpiredProcessor reacts to that event.

Two tasks must cover this, because they reach de-provisioning differently and run at different times:

Task Default schedule Role of the EXPIRED publish
HrEndContractProcess (End of contract validity) 00:50 The primary task. With no workflow configured it calls IdentityContractEndProcessor directly (not via a contract event), so without this publish no guarantee-transfer processor would ever fire for a naturally expired contract. Publishes EXPIRED before processing - also before starting a configured workflow.
IdentityContractExpirationTaskExecutor (Remove roles by expired identity contracts) 01:00 Secondary. Publishes EXPIRED before removing roles. As it runs after HrEndContractProcess, for contracts the HR task already handled it finds nothing left to transfer (idempotent no-op).
The EXPIRED publish is skipped on the very first run of each task (isFirstRun()), to avoid transferring guarantees for all historically expired contracts right after deployment. HrEndContractProcess only publishes for genuine date-expiry (state is null and validTill in the past); contracts invalid for another reason (future validity, disabled / excluded by state) are skipped - they were handled when their state changed. Publishing is wrapped in its own try/catch so a transfer failure can never block de-provisioning.
IdentityContractExpirationEventTriggerTaskExecutor also publishes EXPIRED. Running these tasks together is safe: the transfer is idempotent (a guarantee/holding already moved is detected and not duplicated), so a repeated EXPIRED for the same contract simply finds nothing left to do.

Notification

After a transfer, one consolidated e-mail per new guarantor is sent, listing every role they became a guarantor of and the reason.

Item Value
Topic core:roleGuaranteeTransferred (CoreModuleDescriptor.TOPIC_ROLE_GUARANTEE_TRANSFERRED)
Template roleGuaranteeTransferred (IdmCoreRoleGuaranteeTransferred.xml, system template, bilingual cs/en)
Default delivery e-mail (IdmEmailLog), registered in CoreModuleDescriptor#getDefaultNotificationConfigurations)
Level INFO

Template parameters:

Parameter Type Description
identity IdmIdentityDto the new guarantor (recipient)
roles List<IdmRoleDto> roles the recipient became a guarantor of
originalGuarantee IdmIdentityDto the original guarantor who is no longer active
reason String one of IDENTITY_DELETED, IDENTITY_DISABLED, CONTRACT_DELETED, CONTRACT_DEACTIVATED, CONTRACT_EXPIRED

Configuration

Property Default Description
idm.sec.core.role.guarantee.transferScript roleGuaranteeTransferTargets Code of the system script that resolves the substitutes (managers / superiors).
idm.sec.core.role.guarantee.transferFallbackRole (empty → admin role) Role whose active holders are used as substitutes when the script yields none. The admin role is used when left empty.

Both properties are exposed on the role configuration agenda (RoleConfiguration).

System script: roleGuaranteeTransferTargets

A SYSTEM-category Groovy script that resolves the managers / superiors to whom a guarantee should be transferred.

Parameter Type Description
identity IdmIdentity identity whose managers are resolved (required)
contract IdmIdentityContract optional - when given, only manager(s) reachable through this contract are returned; otherwise all managers of the identity

Returns List<IdmIdentityDto> (never null). It uses IdmIdentityFilter#setManagersFor (all managers) and, when a contract is given, IdmIdentityFilter#setManagersByContract (managers through that contract).

import eu.bcvsolutions.idm.core.api.dto.filter.IdmIdentityFilter;
 
if (identity == null) {
    return java.util.Collections.emptyList();
}
 
IdmIdentityFilter filter = new IdmIdentityFilter();
filter.setManagersFor(identity.getId());
if (contract != null) {
    filter.setManagersByContract(contract.getId());
}
 
return identityService.find(filter, null).getContent();

A custom script can be configured instead (e.g. to return a fixed support team). The contract returned for an inactive contract still resolves managers, because DefaultManagersFilter enforces "valid contract managers" only when explicitly requested.

Transaction and ordering behaviour

Implementation reference

Detection

Type Member Purpose
IdmRoleGuaranteeService findRolesWithExclusiveGuarantee(IdmIdentityDto) Roles for which the identity is the last active direct guarantee (drives whether a substitute is added). Default method returns an empty list; implemented in DefaultIdmRoleGuaranteeService.
IdmRoleGuaranteeService find(filter.setGuarantee(id)) All roles where the identity is a direct guarantee (used to remove it from all of them).
IdmRoleGuaranteeRepository findExclusiveGuaranteeRoleIds(UUID identityId) JPQL (NOT EXISTS on other valid guarantees) backing findRolesWithExclusiveGuarantee.
IdmRoleGuaranteeRoleRepository findExclusivelyHeldGuaranteeRoleIdsByIdentity(UUID, LocalDate) Guarantee roles held only by the given identity (across any contract).
IdmRoleGuaranteeRoleRepository findExclusivelyHeldGuaranteeRoleIdsByContract(UUID, LocalDate) Guarantee roles held only through the given contract.

Transfer

Component Responsibility
RoleGuaranteeTransferManager Central service. transferForRemovedIdentity(…) and transferForDeactivatedContract(…) (both REQUIRES_NEW); direct guarantees removed from all roles (substitute added only for the last-guarantee subset); guarantee-role reassignment; substitute resolution + fallback chain; new-guarantor notification; isContractEffectivelyActive(…).
IdentityGuaranteeTransferDeleteProcessor Identity DELETE.
IdentityGuaranteeTransferDisableProcessor Identity UPDATE into DISABLED_MANUALLY.
IdentityContractGuaranteeTransferDeleteProcessor Active contract DELETE.
IdentityContractGuaranteeTransferDeactivateProcessor Active contract UPDATE (active → inactive).
IdentityContractGuaranteeTransferExpiredProcessor Contract EXPIRED.
HrEndContractProcess Publishes EXPIRED for genuinely date-expired contracts (isFirstRun guard) before its standard de-provisioning / a configured workflow - the primary natural-expiry hook.
HrContractExclusionProcess For a contract excluded outside the application (state set to EXCLUDED directly, so no event fired), calls the transfer manager directly (removeSource = true, isFirstRun guard) before de-provisioning.
IdentityContractExpirationTaskExecutor Publishes EXPIRED (with isFirstRun guard) before removing roles of expired contracts.

Configuration and resources

Artifact Location
Config properties RoleConfiguration / DefaultRoleConfiguration
System script eu/bcvsolutions/idm/scripts/roleGuaranteeTransferTargets.xml
Notification topic CoreModule.TOPIC_ROLE_GUARANTEE_TRANSFERRED + CoreModuleDescriptor default config
E-mail template eu/bcvsolutions/idm/templates/IdmCoreRoleGuaranteeTransferred.xml

Tests

Integration tests (core-impl, JUnit 4 + TestHelper):

Test class Focus
RoleGuaranteeExclusiveDetectionIntegrationTest Detection queries / validity rules.
RoleGuaranteeTransferManagerIntegrationTest Transfer logic and substitute fallback chain, incl. removal of a non-last direct guarantee (without replacement).
RoleGuaranteeTransferNotificationIntegrationTest New-guarantor notification.
RoleGuaranteeTransferIdentityEventIntegrationTest Identity delete / block triggers.
RoleGuaranteeTransferContractEventIntegrationTest Contract delete / deactivate / expire triggers, incl. the expiration LRT and the HR end-contract / exclusion LRT paths.
RoleGuaranteeTransferConfigurationIntegrationTest Config properties, script, template and notification configuration.

Available since 15.15.5.