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:
IdmRoleGuarantee) - an identity is set directly as a guarantor of a role,IdmRoleGuaranteeRole) - the guarantors of a role are all holders of a configured guarantee role.Examples:
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:
VALID and not disabled).validFrom/validTill), it is held through a valid contract (state is null and the contract is valid by validFrom/validTill), and the holding identity is valid (state VALID and not disabled).
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.
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.
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 UPDATE → DISABLED_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 |
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.
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:
For roles that still have another active direct guarantee, the identity is simply removed - no replacement is needed.
For every guarantee role exclusively held by the original identity (resp. through the affected contract):
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.
Substitutes are resolved by a robust fallback chain (RoleGuaranteeTransferManager.resolveSubstitutes):
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.idm.sec.core.role.guarantee.transferFallbackRole (the admin role when not configured).admin identity.If not even the admin identity is available, the affected roles are left without an active guarantor and an error is logged.
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). |
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.
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 |
| 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).
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.
DEFAULT_ORDER - 100) - guarantees, contracts and role assignments still exist, so the last guarantor can be detected and managers resolved.REQUIRES_NEW transaction - the public methods of RoleGuaranteeTransferManager run in their own transaction. Together with the try/catch in each processor this makes the transfer best-effort: it cannot mark the parent (delete / block / expiration) transaction rollback-only.| 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. |
| 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. |
| 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 |
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.