Email Forwarding Upon Termination

Hello,

We want to be able to update ‘ForwardingAddress’ and ‘DeliverToMailboxAndForward’ properties on some Identities Exchange Online accounts when they move into the Sunset Lifecycle State.

Our Entra Source is using the Entra SaaS connector. We have the options for Manage M365 and Exchange Online configured on the source.

In the HR Source there are two values for this need – ‘EmailForward’ (True/False) and 'ForwardingRecipient’ (UPN of recipient).

We have the EXO_ values in the Entra Source Schema and the HR values are added to the Identity Profile as Identity Attributes.

Transform/Attribute Sync doesn’t seem to work because the value will sometimes return empty if the Identity is not marked as requiring forwarding.

Workflow doesn’t seem to work because you can’t manipulate account attributes via a Workflow.

Looking for some guidance on how to achieve this goal. Is anyone else setting up email forwarding upon termination, and if so, how are you accomplishing it?

Thanks.

Hello @chrisk ,

Regarding your following statement “Transform/Attribute Sync doesn’t seem to work because the value will sometimes return empty if the Identity is not marked as requiring forwarding.”

I think the better and standard way for your usecase is to use transform /attributes sync. If value is empty sometime in your HR Source, you can handle this in the transform (for example with firstvalue transform by adding a default value).

Thanks.

A transform seems to be the best way to do this. Do you want to share it so that we can provide feedback?

I may have worded that a bit wrong. The value from the HR source is not empty, the value coming back from the Transform will be empty sometimes which causes an error on the Identity. (I guess Transform can’t return an empty value?)

Here is an example for the ‘DeliverToMailboxAndForward’ Attribute.

If Identity is in ‘Sunset’ LCS AND the HR Attribute ‘emailforward’ is ‘True’, we want to return a value of ‘False’ ELSE don’t return anything.

{
    "name": "deliverAndForward",
    "type": "conditional",
    "attributes": {
        "expression": "$lifecycleState eq sunset",
        "positiveCondition": "$forwardingResult",
        "negativeCondition": "",
        "lifecycleState": {
            "type": "firstValid",
            "attributes": {
                "values": [
                    {
                        "type": "identityAttribute",
                        "attributes": {
                            "name": "cloudLifecycleState"
                        }
                    },
                    {
                        "type": "static",
                        "attributes": {
                            "value": "active"
                        }
                    }
                ]
            }
        },
        "forwardingResult": {
            "type": "conditional",
            "attributes": {
                "expression": "$emailForward eq true",
                "positiveCondition": "false",
                "negativeCondition": "",
                "emailForward": {
                    "type": "firstValid",
                    "attributes": {
                        "values": [
                            {
                                "type": "identityAttribute",
                                "attributes": {
                                    "name": "emailforward"
                                }
                            },
                            {
                                "type": "static",
                                "attributes": {
                                    "value": "false"
                                }
                            }
                        ]
                    }
                }
            }
        }
    },
    "internal": false
}

Since this Attribute will be setup as Attribute Sync – we can’t have it pushing values such as ‘N/A’ or ‘NULL’ etc. to the Exchange Online properties.

Same thing happens with the ‘forwardingRecipient’ transform. We want to Attribute Sync the email to the EXO Property of the Identity if LCS = ‘Sunset’ and emailForward = ‘True’ so we can’t have an arbitrary value pushed to Entra.

{
    "name": "forwardingrecipient",
    "type": "conditional",
    "attributes": {
        "expression": "$lifecycleState eq sunset",
        "positiveCondition": "$forwardingResult",
        "negativeCondition": "",
        "lifecycleState": {
            "type": "firstValid",
            "attributes": {
                "values": [
                    {
                        "type": "identityAttribute",
                        "attributes": {
                            "name": "cloudLifecycleState"
                        }
                    },
                    {
                        "type": "static",
                        "attributes": {
                            "value": "active"
                        }
                    }
                ]
            }
        },
        "forwardingResult": {
            "type": "conditional",
            "attributes": {
                "expression": "$emailForward eq true",
                "positiveCondition": "$recipientValue",
                "negativeCondition": "",
                "emailForward": {
                    "type": "firstValid",
                    "attributes": {
                        "values": [
                            {
                                "type": "identityAttribute",
                                "attributes": {
                                    "name": "emailforward"
                                }
                            },
                            {
                                "type": "static",
                                "attributes": {
                                    "value": "false"
                                }
                            }
                        ]
                    }
                },
                "recipientValue": {
                    "type": "firstValid",
                    "attributes": {
                        "values": [
                            {
                                "type": "accountAttribute",
                                "attributes": {
                                    "sourceName": "CORP HR",
                                    "attributeName": "ForwardingRecipient"
                                }
                            },
                            {
                                "type": "static",
                                "attributes": {
                                    "value": ""
                                }
                            }
                        ]
                    }
                }
            }
        }
    },
    "internal": false
}

These work for an Identity that is terminated and moves to ‘Sunset’, but causes all other Identities to error because of the ‘blank’.

Was really hoping to use Attribute Sync, but haven’t figured it out.

Any thoughts or suggestions?

Thanks!

For all others cases that sunset,what if you retrived the current exo property ?

Like :

{
    "name": "deliverAndForward",
    "type": "static",
    "attributes": {
        "value": "#if($lifecycleState == 'sunset' && $emailForward == 'true')false#{else}$exoForwardEnabled#end",
        "lifecycleState": {
            "type": "firstValid",
            "attributes": {
                "values": [
                    {
                        "type": "identityAttribute",
                        "attributes": {
                            "name": "cloudLifecycleState"
                        }
                    },
                    {
                        "type": "static",
                        "attributes": {
                            "value": "active"
                        }
                    }
                ]
            }
        },
        "emailForward": {
            "type": "firstValid",
            "attributes": {
                "values": [
                    {
                        "type": "identityAttribute",
                        "attributes": {
                            "name": "emailforward"
                        }
                    },
                    {
                        "type": "static",
                        "attributes": {
                            "value": "false"
                        }
                    }
                ]
            }
        },
        "exoForwardEnabled": {
            "type": "accountAttribute",
            "attributes": {
                "sourceName": "Exchange Online",
                "attributeName": "DeliverToMailboxAndForwardAttribute"
            }
        }
    },
    "internal": false
}

This will keep Identity Attribute and account attribute always same and sync will not triggered expect in sunset case

Also one remark :

  • In A transform expect lifecycleState, a transform associated to Identity attributes should always prioritize the account attributes if possible :
  • Like instead of : {
    “type”: “identityAttribute”,
    “attributes”: {
    “name”: “emailforward”
    }
    }
  • I should use if possible : {
    “type”: “accountAttribute”,
    “attributes”: {
    “sourceName”: “Your HR Source Name”,
    “attributeName”: “emailforwardAttribute”
    }
    }

I’ll give this a try and report back results, thank you!


Tried this with high hopes, however, I am still getting : “sailpoint.api.AttributePromotionException: Applying transform[reference] to attribute[deliverAndForward] failed”

I’m thinking it is because the value on the actual EXO accounts are also blank by default until populated. So this too is trying to return a ‘blank’ for the Transform.

Morning,
I wrote a piece recently about how to send ‘blank’ results from a transform. If you ‘trim’ a ‘space’ that comes from a static transform, that works.
Try this:

{
  "name": "deliverAndForward",
  "type": "trim",
  "attributes": {
    "input": {
      "type": "static",
      "attributes": {
        "value": "#if($lifecycleState == 'sunset' && $emailForward == 'true')false#{else} #end",
        "lifecycleState": {
          "type": "identityAttribute",
          "attributes": {
            "name": "cloudLifecycleState"
          }
        },
        "emailForward": {
          "type": "firstValid",
          "attributes": {
            "values": [
              {
                "type": "identityAttribute",
                "attributes": {
                  "name": "emailforward"
                }
              },
              "false"
            ]
          }
        }
      }
    }
  }
}

EDIT: You might need to have an {elseif} in there. I know for a fact that this outputs a ‘blank’, but not tried it as a standalone statement

#{elseif}($oldValue == '[NO EMAIL]') #{else}#end",

A better way would be to use a firstValid where the final value is just null.

I don’t believe that null will be passed through under a firstValid function.
At least I’ve never been able to get that to work

Check out this discussion to get a firstValid transform to return a null value: https://developer.sailpoint.com/discuss/t/transform-to-return-null/30233/11

Thanks for the link @zachm117 - I think I followed the logic, not sure I did it exactly the same - but currently ‘seems’ to be working!?! At least it isn’t throwing an error any longer.

{
    "id": "d87571ce-f2e0-40a6-8bfd-30189c1e093b",
    "name": "deliverAndForwardv3",
    "type": "firstValid",
    "attributes": {
        "values": [
            {
                "type": "conditional",
                "attributes": {
                    "expression": "$lifecycleState eq sunset",
                    "positiveCondition": "$forwardingResult",
                    "negativeCondition": "$null",
                    "null": {
                        "type": "static",
                        "attributes": {
                            "value": null
                        }
                    },
                    "lifecycleState": {
                        "type": "firstValid",
                        "attributes": {
                            "values": [
                                {
                                    "type": "identityAttribute",
                                    "attributes": {
                                        "name": "cloudLifecycleState"
                                    }
                                },
                                {
                                    "type": "static",
                                    "attributes": {
                                        "value": "active"
                                    }
                                }
                            ]
                        }
                    },
                    "forwardingResult": {
                        "type": "conditional",
                        "attributes": {
                            "expression": "$emailForward eq true",
                            "positiveCondition": "false",
                            "negativeCondition": "$null",
                            "emailForward": {
                                "type": "firstValid",
                                "attributes": {
                                    "values": [
                                        {
                                            "type": "identityAttribute",
                                            "attributes": {
                                                "name": "emailforward"
                                            }
                                        },
                                        {
                                            "type": "static",
                                            "attributes": {
                                                "value": "false"
                                            }
                                        }
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        ]
    },
    "internal": false
}

Here is the result on an identity that was previously erroring with my original attempts -

I’m going to push a few more test thru and HOPE this actually ends up working. I’ll come back and mark post as solution.

Thanks to you all for your great help and replies!

No problem @chrisk !

I wouldn’t think this block of code would work, but I can definitely be wrong.

                    "null": {
                        "type": "static",
                        "attributes": {
                            "value": null
                        }
                    },

If this is working for you, awesome! If not, here is how I would have probably written this transform in case it is helpful. Mine has the parent firstValid returning a null value if the first value in the attributes list (the static transform) results in an error, which it will anytime it gets to the else clause.

Yours has a static ‘null’ which usually would return an error (as far as I understood it) and I would have thought would cause the first value in your firstValid attributes (your only value) to result in an error, but that doesn’t appear to be the case. I am curious to see if yours ends up working though after your validation. Although as I am writing this, I guess if you tacked on a null as your second value in your firstValid, it would help catch it like mine does if your first value ends up erroring. Might be the best of both worlds just using conditionals vs VTL.

{
    "name": "DevRel Null",
    "type": "firstValid",
    "attributes": {
        "values": [
            {
                "type": "static",
                "attributes": {
                    "value": "#if($lifecycleState == 'sunset' && $emailForward == 'true')false#{else}$null#end",
                    "lifecycleState": {
                        "type": "firstValid",
                        "attributes": {
                            "values": [
                                {
                                    "type": "identityAttribute",
                                    "attributes": {
                                        "name": "cloudLifecycleState"
                                    }
                                },
                                {
                                    "type": "static",
                                    "attributes": {
                                        "value": "active"
                                    }
                                }
                            ]
                        }
                    },
                    "emailForward": {
                        "type": "firstValid",
                        "attributes": {
                            "values": [
                                {
                                    "type": "identityAttribute",
                                    "attributes": {
                                        "name": "emailForward"
                                    }
                                },
                                {
                                    "type": "static",
                                    "attributes": {
                                        "value": "false"
                                    }
                                }
                            ]
                        }
                    },
                    "null": null
                }
            },
            null
        ]
    }
}

Please let me know how yours turns out!

  • Zach

@zachm117 You are 100% correct in that it didn’t actually end up working - it was just masking the errors I was previously getting. After hours with Claude and no path forward I saw your post and I copied it into my prompt. It ‘Appears’ that we have a working path now!

Claude says -

That’s fantastic! The Ambassador’s insight was the key — using the Velocity template inside the static transform with $null in the else clause was the right approach all along.

So to summarize what the final solution was:

  • Ditch the nested conditionals for null handling

  • Use a single static transform with a Velocity template (#if/#else/#end)

  • Use $null in the else clause rather than trying to return an empty string or a static null value

  • Wrap in firstValid with null as the second value as a safety net

I’ve only tested on three Identities so far, but I’m marking this as solved until I hit another wall.

Cant thank you, and all the others, enough!

Chris

For anyone that cares to see the final ‘Working’ Transform -

{
    "id": "d87571ce-f2e0-40a6-8bfd-30189c1e093b",
    "name": "deliverAndForwardv3",
    "type": "firstValid",
    "attributes": {
        "values": [
            {
                "type": "static",
                "attributes": {
                    "value": "#if($lifecycleState == 'sunset' && $emailForward == 'true')false#{else}$null#end",
                    "lifecycleState": {
                        "type": "firstValid",
                        "attributes": {
                            "values": [
                                {
                                    "type": "identityAttribute",
                                    "attributes": {
                                        "name": "cloudLifecycleState"
                                    }
                                },
                                {
                                    "type": "static",
                                    "attributes": {
                                        "value": "active"
                                    }
                                }
                            ]
                        }
                    },
                    "emailForward": {
                        "type": "firstValid",
                        "attributes": {
                            "values": [
                                {
                                    "type": "identityAttribute",
                                    "attributes": {
                                        "name": "emailforward"
                                    }
                                },
                                {
                                    "type": "static",
                                    "attributes": {
                                        "value": "false"
                                    }
                                }
                            ]
                        }
                    },
                    "null": null
                }
            },
            null
        ]
    },
    "internal": false
}

Seems so simple, but took a village! <3

Ah okay that makes sense, great to hear it sounds like it is working now! A word of caution though, to reiterate what some other folks said, it is not recommended to use identityAttributes in another identity attribute transform. You can see the documentation here: Identity Attribute | SailPoint Developer Community

  • This transform is not intended for use within an another identity profile attribute’s calculation. Identity attribute calculations are multi-threaded processes, and there is no guarantee that a specific attribute has current data, or even exists, at the time of calculation within any given transform. Referencing identity attributes within another identity attribute’s calculation can lead to identity exceptions.

I assume for something like your cloudLifecycleState identity attribute, you are using another transform to calculate that attribute (active, sunset, etc.). Instead of referencing the cloudLifecycleState identity attribute, you can instead reference the transform you are using for the couldLifecycleState identity attribute directly using the Reference operation. Basically, instead of referencing the identity attribute, reuse the transform to calculate the lifecycle state again in this transform.

For your emailforward identityAttribute, if you are using a transform on some source/account attribute, you can do the same thing I mentioned above for cloudLifecycleState. If you are just referencing the account attribute directly in this identity attribute, this transform should instead reference the Account Attribute instead of the identityAttribute.

Using the identityAttribute operation inside another identity attribute transform can work, but you might run into some unexpected timing issues or errors down the line due to the multi-threaded processes. I’d recommend changing this now to avoid any unexpected problems in the future. Let me know if you need a hand with this.

  • Zach