Table of Contents

, , , , ,

Automatic Role Guarantor Transfer

@since 15.17.0

When a role guarantor stops being able to act as a guarantor (the identity is deleted or blocked, one of its contracts is deleted, deactivated or expires, or the guarantor loses the guarantee role assignment itself), 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 IdStory:

Examples:

Key concepts

Last (active) guarantor of the given type

Anything happens only when the affected identity is the last active guarantor - if somebody else still guarantees the role, there is nothing to solve and nothing is changed (the inactive identity simply stops counting as an active guarantor). 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.

Transfer marker (anti-loop)

The internal role requests created by the transfer are marked with the event property RoleGuaranteeTransferManager.PROPERTY_GUARANTEE_TRANSFER (`idm:role-guarantee-transfer`). The marker propagates to the role-assignment events of the request's concepts, so the assignment-loss triggers recognize and skip changes caused by the transfer itself - otherwise the transfer's own source-removal would recursively trigger another transfer.

When the transfer runs (scenario matrix)

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.

The following matrix describes the action taken in each situation when the affected user is the last active direct guarantor of some role or the last effective holder of a guarantee role. When another active guarantor / effective holder remains, nothing is transferred and nothing is changed. In every situation the substitute(s) are resolved by the configured script with its fallback chain (the fallback role holders and the admin identity as the last resort) - see How a substitute is resolved; which parameters the transfer passes to the resolution (and whose managers the bundled default script therefore returns) is described in System script:

EventLast direct guarantor of a roleLast holder of a guarantee role
The user is deletedThe guarantor is replaced by the resolved substitute(s).The guarantee role is assigned to the resolved substitute(s).
The user is manually blockedThe resolved substitute(s) are added as a guarantor; the blocked user keeps its guarantee.The guarantee role is assigned to the resolved substitute(s); the blocked user keeps its holding.
The user's active contract is deletedWhen it was the user's last active contract (the user becomes inactive), the guarantor is replaced by the resolved substitute(s).When the guarantee role was held on the deleted contract, it is assigned to the resolved substitute(s).
The user's active contract expires (its validTill passes)When it was the user's last active contract, the guarantor is replaced by the resolved substitute(s) (driven by the nightly tasks - see Natural contract expiry).When the guarantee role was held on the expired contract, it is assigned to the resolved substitute(s).
The user's active contract validity is moved to the future (by an edit)When it was the user's last active contract, the guarantor is replaced by the resolved substitute(s).When the guarantee role was held on the changed contract, it is assigned to the resolved substitute(s); the original holding is removed.
The user's active contract validity is moved to the past (by an edit)When it was the user's last active contract, the guarantor is replaced by the resolved substitute(s).When the guarantee role was held on the changed contract, it is assigned to the resolved substitute(s); the original holding is removed.
The user's active contract is switched to "Excluded"When it was the user's last active contract (the user becomes inactive), the resolved substitute(s) are added as a guarantor; the user keeps its guarantee. When another active contract remains, the user stays valid and nothing is changed.When the guarantee role was held on the excluded contract, it is assigned to the resolved substitute(s); the user keeps its holding.
An active contract is set to "Excluded" outside the application (e.g. directly in the database)The nightly HrContractExclusionProcess runs the transfer before its de-provisioning, with the same result as an exclusion by edit.The same as an exclusion by edit - the substitute is assigned the guarantee role and the user keeps its holding.
The user's active contract is switched to "Disabled"When it was the user's last active contract, the guarantor is replaced by the resolved substitute(s).When the guarantee role was held on the changed contract, it is assigned to the resolved substitute(s); the original holding is removed.
The user is manually removed as a direct guarantor of a roleNo reaction - a deliberate manual change is respected.N/A
The guarantee role assignment validity is set to start in the future or end in the past (by an edit)N/AOn saving a change that invalidates the assignment, the guarantee role is immediately assigned to the resolved substitute(s); the assignment itself is left with the validity the user has set.
The guarantee role assignment expires (its validTill passes)N/AThe date passing raises no event; the nightly Remove expired roles task removes the assignment and the removal triggers the transfer - the guarantee role is assigned to the resolved substitute(s).
The guarantee role is removed from the userN/AEvery removal path (role request, bulk action, recertification, synchronization, automatic role recalculation, contract / identity delete cascades) ends in the assignment delete event: the guarantee role is assigned to the resolved substitute(s). The transfer's own requests are excluded (the anti-loop marker).
The role stops being a guarantee role (the owner role link is removed manually)N/ANo reaction - a deliberate manual change is respected.
The guarantee role is deleted (manually from the GUI)N/ANo reaction - the transfer is deliberately skipped for a role that is being deleted (the DELETED entity state guard); the guarantees disappear together with the role.
The guarantee role is deleted (e.g. by an import)N/ANo reaction - the same guard as for the manual delete applies.

The triggers are implemented by the following processors:

TriggerEventProcessorRemoves original holding?Reason code
Identity is deletedIdentity DELETE IdentityGuaranteeTransferDeleteProcessor No - the delete cascade removes itIDENTITY_DELETED
Identity is manually blockedIdentity UPDATE to DISABLED_MANUALLY IdentityGuaranteeTransferDisableProcessor No - the blocked identity keeps everything it guaranteesIDENTITY_DISABLED
Active contract is deletedContract DELETE IdentityContractGuaranteeTransferDeleteProcessor No - the delete cascade removes itCONTRACT_DELETED
Active contract is deactivated by an edit (validTill set to the past, validFrom set to the future, set DISABLED)Contract UPDATE (active → inactive)IdentityContractGuaranteeTransferDeactivateProcessor Yes - the identity loses the contract's role assignmentsCONTRACT_DEACTIVATED
Active contract is set EXCLUDED by an editContract UPDATE (active → excluded)IdentityContractGuaranteeTransferDeactivateProcessor No - exclusion keeps the role assignments, so the guarantees are kept tooCONTRACT_EXCLUDED
Contract expires naturally (its validTill passes)Contract EXPIRED IdentityContractGuaranteeTransferExpiredProcessor No - the expiration task removes itCONTRACT_EXPIRED
Guarantee role assignment is removed (role request, bulk action, recertification, synchronization, automatic role recalculation, assignment expiration task, contract / identity delete cascades)Role assignment DELETE IdentityRoleGuaranteeTransferDeleteProcessor No - the triggering removal deletes itASSIGNMENT_REMOVED
Guarantee role assignment loses validity by an edit (validFrom moved to the future, validTill to the past)Role assignment UPDATE (valid → invalid)IdentityRoleGuaranteeTransferDeactivateProcessor No - the assignment stays with the validity the user has setASSIGNMENT_DEACTIVATED
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). Guarantee roles are always evaluated per the affected contract or assignment.
Besides these event processors, two nightly long running tasks (LRT) 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 and guarantee roles are evaluated independently - each one is transferred on its own, so a role can get a substitute for one kind of guarantorship while the other one stays untouched. Both are, however, subject to the same two rules below.

The reversible administrative states keep everything the identity guarantees even when it was the last guarantor - the substitute is added, but neither the direct guarantee nor the guarantee role is taken away. This applies to a manually blocked identity and to a contract excluded from the headcount: both keep the identity's role assignments, so the identity gets its guarantorship back when it is unblocked / included again. While in that state the identity is not valid, so it does not count as an active guarantor and the roles are guarded by the substitute in the meantime.

A contract switched to Disabled or invalidated by date behaves the other way round - it takes the contract's role assignments away, so the replaced guarantees and guarantee role holdings are removed as well.
⚠️ Reverting a block or an exclusion does not revert the transfer - neither for direct guarantees nor for guarantee roles. When a manual block or a contract exclusion triggers the transfer and the action is later undone (the identity is unblocked, the contract is included again), the original guarantor becomes active again - but the substitute stays: it keeps the direct guarantee it was given and it keeps the guarantee role it was assigned. The role therefore ends up with both guarantors. The transfer only ever adds a guarantor; nothing removes the substitute automatically, because IdM cannot tell an unwanted leftover from a guarantor somebody added deliberately. If the role should have the original guarantor only, remove the substitute's direct guarantee and unassign its guarantee role manually.

Direct guarantees

Only the guarantees that are actually replaced are touched. For every guarantee where the identity was the last active direct guarantee of the given role and type:

  1. the substitute(s) are added as a direct guarantee of the role with the inherited type (if they do not guarantee the role with that type yet), then
  2. the replaced guarantee of the original identity is removed.

A guarantee whose role keeps another active guarantee of the same type is left in place - there is nothing to replace, and the inactive identity does not count as an active guarantor anyway. This mirrors guarantee roles: an inactive holder also keeps a guarantee role that somebody else still holds. (When the identity is deleted, all its guarantees are removed by the identity delete itself, independently of the transfer.)

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 (or held only through the affected contract or assignment):

  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 - including the guarantee type defined on each owner role link,
  3. the original holding is removed when removeSource applies (see the matrix 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.

Loss of the guarantee role assignment

Since the guarantors of a role can be all holders of a guarantee role, the role can lose its last guarantor also when the holding itself disappears - without the identity or contract becoming inactive. Two processors on the role-assignment events cover this:

Substitutes are resolved through the contract of the lost assignment. The transfer is skipped when:

The assignment-level triggers overlap with the identity / contract level ones on cascades (e.g. a contract delete removes its assignments). This is safe: the earlier trigger installs the substitute, so by the time the assignment DELETE fires, the holding is no longer the last one and the transfer is a no-op. At the same time the assignment trigger acts as a safety net when the earlier transfer could not run.

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:

TaskDefault scheduleRole of the EXPIRED publish
HrEndContractProcess (End of contract validity)00:50The 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:00Secondary. 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.

Traceability of the transfer requests

The internal role requests created by the transfer are self-describing, so anyone reviewing them later understands why the guarantee role was moved automatically:

Notification

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

ItemValue
Topiccore:roleGuaranteeTransferred (CoreModuleDescriptor.TOPIC_ROLE_GUARANTEE_TRANSFERRED)
TemplateroleGuaranteeTransferred (IdmCoreRoleGuaranteeTransferred.xml, system template, bilingual cs/en body)
Subject`IdStory - stali jste se garantem rolí`
Default deliverye-mail (IdmEmailLog), registered in CoreModuleDescriptor#getDefaultNotificationConfigurations
LevelINFO

The body follows the standard IdStory e-mail layout: an impersonal greeting, the original guarantor referenced as first name last name (username), the affected roles listed by name, a human-readable reason sentence and a contact hint ("Pokud s přidělením garantství nesouhlasíte, kontaktujte prosím administrátory IdM. / If you disagree with the guarantorship assignment, please contact your IdM administrators.").

Template parameters:

ParameterTypeDescription
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_EXCLUDED, CONTRACT_EXPIRED, ASSIGNMENT_REMOVED, ASSIGNMENT_DEACTIVATED

Audit

Both guarantee entities implement AuditSearchable with the guaranteed role as the audit owner:

EntityOwnerSub owner
IdmRoleGuarantee the guaranteed rolethe guarantee identity
IdmRoleGuaranteeRole the guaranteed (owner) rolethe guarantee role

The role detail audit tab queries audit records by the related owner (relatedOwnerId = role identifier), so every create / update / delete of a role's guarantee - including the changes made by the automatic transfer - is visible directly on the role detail, tab Audit.

How to recognize an automatic guarantor transfer

When you need to find out why somebody is a guarantor of a role ("who made this person a guarantor?"), look for these traces:

Configuration

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

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

The transfer passes the parameters according to the trigger - with the default script this determines whose managers become the substitutes:

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 given for an inactive contract still resolves managers, because DefaultManagersFilter enforces "valid contract managers" only when explicitly requested.

Transaction and ordering behaviour

Implementation reference

Detection

TypeMemberPurpose
IdmRoleGuaranteeService findExclusiveGuarantees(IdmIdentityDto) Direct guarantees of the identity for which it is the last active guarantee of the given role and type (drives whether a substitute is added and with which type). Default method returns an empty list; implemented in DefaultIdmRoleGuaranteeService.
IdmRoleGuaranteeService find(filter.setGuarantee(id)) All direct guarantees of the identity (used to remove it from all of them).
IdmRoleGuaranteeRepository findExclusiveGuarantees(UUID identityId) JPQL (NOT EXISTS on other valid guarantees of the same type; null type is a type of its own) backing the above.
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.
IdmRoleGuaranteeRoleRepository findExclusivelyHeldGuaranteeRoleIdsByAssignment(UUID, UUID, LocalDate) Guarantee role whose only effective holding is the given role assignment (the assignment itself is not filtered by validity).

Transfer

ComponentResponsibility
RoleGuaranteeTransferManager Central service. transferForRemovedIdentity(…), transferForDeactivatedContract(…) and transferForRemovedAssignment(…) (all @Transactional, joining the triggering operation's transaction); direct guarantees replaced per role and type (substitute added with the inherited type, only the replaced guarantee removed); guarantee-role reassignment; substitute resolution + fallback chain; request description; new-guarantor notification; isContractEffectivelyActive(…); PROPERTY_GUARANTEE_TRANSFER anti-loop marker; guard for a role being (force) deleted.
IdentityGuaranteeTransferDeleteProcessor Identity DELETE.
IdentityGuaranteeTransferDisableProcessor Identity UPDATE into DISABLED_MANUALLY.
IdentityContractGuaranteeTransferDeleteProcessor Active contract DELETE.
IdentityContractGuaranteeTransferDeactivateProcessor Active contract UPDATE (active → inactive).
IdentityContractGuaranteeTransferExpiredProcessor Contract EXPIRED.
IdentityRoleGuaranteeTransferDeleteProcessor Role assignment DELETE (any removal path).
IdentityRoleGuaranteeTransferDeactivateProcessor Role assignment UPDATE (valid → invalid).
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 = false, isFirstRun guard) before de-provisioning.
IdentityContractExpirationTaskExecutor Publishes EXPIRED (with isFirstRun guard) before removing roles of expired contracts.

Configuration and resources

ArtifactLocation
Config propertiesRoleConfiguration / DefaultRoleConfiguration
System scripteu/bcvsolutions/idm/scripts/roleGuaranteeTransferTargets.xml
Notification topicCoreModule.TOPIC_ROLE_GUARANTEE_TRANSFERRED + CoreModuleDescriptor default config
E-mail templateeu/bcvsolutions/idm/templates/IdmCoreRoleGuaranteeTransferred.xml
Audit ownersIdmRoleGuarantee / IdmRoleGuaranteeRole implement AuditSearchable

Tests

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

Test classFocus
RoleGuaranteeExclusiveDetectionIntegrationTest Detection queries / validity rules, incl. per-type exclusivity and the null-type bucket.
RoleGuaranteeTransferManagerIntegrationTest Transfer logic and substitute fallback chain, incl. type inheritance, removal of a non-last direct guarantee (without replacement) and the request description.
RoleGuaranteeTransferNotificationIntegrationTest New-guarantor notification (consolidation, role names, contact hint).
RoleGuaranteeTransferIdentityEventIntegrationTest Identity delete / block triggers.
RoleGuaranteeTransferContractEventIntegrationTest Contract delete / deactivate / expire triggers, incl. the expiration LRT and the HR end-contract / exclusion LRT paths.
RoleGuaranteeTransferIdentityRoleEventIntegrationTest Assignment-loss triggers (delete, validity change, assignment expiry LRT), anti-loop marker, deleted-role guard.
RoleGuaranteeAuditIntegrationTest Audit owner / sub owner of both guarantee entities, role detail audit reachability.
RoleGuaranteeTransferConfigurationIntegrationTest Config properties, script, template and notification configuration.