{{tag> topic role guarantor guarantee transfer idm-core }}
====== 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:
* **direct guarantee** (''IdmRoleGuarantee'') - an identity is set directly as a guarantor of a role; the guarantee carries an optional **guarantee type** (codelist `guarantee-type`),
* **guarantee role** (''IdmRoleGuaranteeRole'') - the guarantors of a role are all holders of a configured //guarantee role//; the guarantee type is a property of the owner role - guarantee role link.
//Examples://
* //A team lead who is the sole guarantor of several application roles leaves the company and their identity is deleted - the guarantees move to their manager.//
* //An external consultant's contract expires; they were the only holder of a guarantee role used by ten business roles - the guarantee role is reassigned to the manager reachable through that contract.//
* //A group synchronization removes the guarantee role from its last holder - the guarantee role is reassigned to the holder's manager.//The transfer is **atomic with the operation that triggers it** - it joins that operation's transaction. When the operation is rejected or fails (for example a contract controlled by contract slices cannot be deleted), the transfer is rolled back with it and the original guarantor is kept. Conversely, a failed transfer aborts the triggering operation instead of letting a role silently lose its guarantor.
===== 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:
* **Direct guarantee** - guarantee types are evaluated **separately** (a ''null'' type is a type of its own): another guarantee counts only if it has the **same type** and its identity is **valid** (state ''VALID'' and not disabled). The substitute inherits the type of the guarantee it replaces, so the role keeps an active guarantor of each type.
* **Guarantee role** - another holder counts only if //all// of the following hold: the role assignment is currently valid (''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). The holding itself carries no type - the type lives on the owner role - guarantee role link, so the substitute inherits it automatically by holding the guarantee role.
==== 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|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|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_roleguaranteetransfertargets|System script]]:
^Event^Last direct guarantor of a role^Last holder of a guarantee role|
|The user is deleted|The guarantor is replaced by the resolved substitute(s).|The guarantee role is assigned to the resolved substitute(s).|
|The user is manually blocked|The 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 deleted|When 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|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 role|No 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/A|On 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/A|The 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 user|N/A|Every 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/A|No reaction - a deliberate manual change is respected.|
|The guarantee role is deleted (manually from the GUI)|N/A|No 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/A|No reaction - the same guard as for the manual delete applies.|
The triggers are implemented by the following processors:
^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'' to ''DISABLED_MANUALLY'' |''IdentityGuaranteeTransferDisableProcessor'' |No - the blocked identity keeps everything it guarantees|''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 ''DISABLED'')|Contract ''UPDATE'' (active → inactive)|''IdentityContractGuaranteeTransferDeactivateProcessor'' |Yes - the identity loses the contract's role assignments|''CONTRACT_DEACTIVATED'' |
|Active contract is set ''EXCLUDED'' by an edit|Contract ''UPDATE'' (active → excluded)|''IdentityContractGuaranteeTransferDeactivateProcessor'' |No - exclusion keeps the role assignments, so the guarantees are kept too|''CONTRACT_EXCLUDED'' |
|Contract expires naturally (its ''validTill'' passes)|Contract ''EXPIRED'' |''IdentityContractGuaranteeTransferExpiredProcessor'' |No - the expiration task removes it|''CONTRACT_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 it|''ASSIGNMENT_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 set|''ASSIGNMENT_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|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**:
- 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
- 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):
- 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),
- 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,
- 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:
* **Removal** (''IdentityRoleGuaranteeTransferDeleteProcessor'') - every removal path in IdStory ends in the role-assignment ''DELETE'' event, so a single processor covers a role request (manual removal, bulk action, recertification), group synchronization, automatic role recalculation, the assignment expiration task (''IdentityRoleExpirationTaskExecutor'') and the cascades of contract / identity removal (including business-role sub-roles). The processor runs before the row is deleted, so the last-holding detection still sees it; the removed assignment itself is intentionally **not** checked for validity, so an already expired holding removed later by the expiration task is still detected as the last one.
* **Validity change** (''IdentityRoleGuaranteeTransferDeactivateProcessor'') - when an update moves the assignment validity to the past or to the future, the assignment stops being an effective holding. The assignment itself is left untouched - it stays with the validity the user has set (an assignment expired by date is removed later by the standard expiration task); only the substitute is assigned the guarantee role.
Substitutes are resolved through the contract of the lost assignment. The transfer is skipped when:
* the change was caused by the transfer itself (the [[#transfer_marker_anti-loop|transfer marker]]),
* another effective holder of the guarantee role remains (including the same identity holding it on another contract),
* the assigned role is being (force) deleted - the role is flagged with the ''DELETED'' entity state and transferring a guarantee of a role that is about to disappear makes no sense.
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''):
- **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.
- **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).
- **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.
===== 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:
* **Description** (user-visible on the role request detail) - a short English sentence with the reason code, different for the two operations: `Automatic role guarantor transfer - guarantee role assigned to a substitute (CONTRACT_DEACTIVATED).` and `Automatic role guarantor transfer - guarantee role removed from the original guarantor (CONTRACT_DEACTIVATED).`
* **Log** (visible in the admin mode of the request detail) - `Request was created by automatic role guarantee transfer.`
* **Requested by** - ''AUTOMATICALLY'', with the system implementer.
===== 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 body)|
|Subject|`IdStory - stali jste se garantem rolí`|
|Default delivery|e-mail (''IdmEmailLog''), registered in ''CoreModuleDescriptor#getDefaultNotificationConfigurations'' |
|Level|''INFO'' |
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:
^Parameter^Type^Description|
|''identity'' |''IdmIdentityDto'' |the new guarantor (recipient)|
|''roles'' |''List'' |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**:
^Entity^Owner^Sub owner|
|''IdmRoleGuarantee'' |the guaranteed role|the guarantee identity|
|''IdmRoleGuaranteeRole'' |the guaranteed (owner) role|the 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:
* **Role requests** of the person (identity detail, tab Roles / role requests): the transfer requests carry the description `Automatic role guarantor transfer - guarantee role assigned to a substitute ()`, are requested ''AUTOMATICALLY'' and have the system implementer.
* **Notification log** (Audit, Notifications): the new guarantor received an e-mail with the topic ''core:roleGuaranteeTransferred'' listing the roles and the reason.
* **Role detail, tab Audit**: the creation of the new ''IdmRoleGuarantee'' / assignment changes are recorded with the role as the audit owner.
* **Server log**: the transfer writes ''INFO'' records (`Role […] direct guarantee of type […] transferred to substitute identity […]`, `Guarantee role […] assigned to substitute identity […]`).
===== 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'' (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:
* **identity-level events** (the user is deleted / manually blocked) - for **direct guarantees** no contract is given, so the default script returns **all managers of the user**; for **guarantee roles** the contract through which the guarantee role was held is given (each of them, when it was held on several contracts), so only the **manager(s) reachable through that contract** are returned,
* **contract-level events** (contract delete / deactivation / expiry) - the affected contract is given, so the default script returns the manager(s) reachable through it,
* **assignment-level events** (loss of the guarantee role assignment) - the contract of the lost assignment is given.
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 =====
* **BEFORE phase, negative order** (''DEFAULT_ORDER - 100'') - guarantees, contracts and role assignments still exist, so the last guarantor can be detected and managers resolved.
* **Atomic with the triggering operation** - the public methods of ''RoleGuaranteeTransferManager'' join the transaction of the operation that triggered them. An operation that is rejected or fails after the transfer ran (a validation such as //a contract controlled by slices cannot be deleted//, a constraint violation, an error while saving a contract during synchronization) rolls the transfer back as well, so the original guarantor is never silently replaced by a change that did not happen. The price is the opposite behaviour: a failed transfer aborts the triggering operation with an error instead of continuing without a guarantor.
* **Synchronous internal requests** - the role requests the transfer creates are published with ''PriorityType.IMMEDIATE'', so they are executed inside the same transaction. This is what keeps the transfer atomic (an asynchronous request would run later in its own transaction and survive a rollback) and what makes the overlapping triggers of one operation see each other's result.
* **Idempotent across the triggers of one operation** - one operation fires several triggers (deleting an identity also fires the contract and role-assignment events). The first trigger transfers; because its result is immediately visible, the exclusivity checks of the later triggers find the substitute as an effective holder / guarantee and do nothing, so neither the guarantee role nor the notification is duplicated. Assigning a guarantee / guarantee role that already exists is additionally detected and skipped.
* **Anti-loop** - the transfer's own role requests carry the [[#transfer_marker_anti-loop|transfer marker]], so the assignment-loss triggers never react to them.
===== Implementation reference =====
==== Detection ====
^Type^Member^Purpose|
|''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 ====
^Component^Responsibility|
|''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 ====
^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'' |
|Audit owners|''IdmRoleGuarantee'' / ''IdmRoleGuaranteeRole'' implement ''AuditSearchable'' |
===== Tests =====
Integration tests (''core-impl'', JUnit 4 + ''TestHelper''):
^Test class^Focus|
|''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.|