The Attributes coming from the SAML Identity Provider can be transformed using Groovy-scripts.

If you upgraded from version 4.x and already used Groovy-transformation, check migrate

General Information


General information about Groovy can be found here: https://groovy-lang.org/documentation.html


In the IdP's Attribute Mapping configuration, select Groovy Code as Source type and you can enter the script-code:


Access attribute values

All attributes coming from the SAML Identity Provider are available in the Script's Binding (in a Groovy-script, the Binding is the set of the Script's variables), so they are "just there" as variable. If the attribute name is not a valid identifier in Groovy, it can be addressed with mapping.'attribute name', e.G.

mapping.'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
CODE

The SAML NameID-value can be accessed under the special attribute ATTR_NAMEID.

Returning data

The value returned from the script is used as value for the mapped attribute. Some conversion automatically happens:

In a Groovy-Script, the last statement's result is returned automatically, so 

return someValue
CODE

can just be written as

someValue
CODE

Ignore an attribute

If the script returns the special value DROP, the mapped attribute is not used to create or update the user. If an existing user already has this attribute, it is kept as it is.

Remove the attribute

If the script returns null or the special value EMPTY, the attribute on an existing user is removed.

Filter the user

If a user should be filtered, return the special value DROP_ALL. In this case, an existing user will be disabled and the authentication for the user will fail.


Logging

There is an SLF4j-logger available as logger:

logger.warn("Hello World!")
CODE

will occur in the logs log this:

2021-05-18 16:00:56,774+0000 ForkJoinPool.commonPool-worker-3 WARN      [d.r.retransform.impl.GroovyAttributeTransformerScript] Hello World!
CODE


Special Implementation - Distinguish whether a user has logged into the JSM portal or into Jira

In SAML 5 we have implemented an additional attribute ATTR_SD_CUSTOMER. This attribute is not coming from the IdP but is set to "true" or "false" depending on the relative URL used by the user to sign into Jira.

To use the attribute you first need to add a new User Property in the Attribute mapping table of the Identity Provider tab.


Set the Jira attribute name and click Next

Enter the name of the attribute in the SAML response and click Apply


When you now login you will see this value being returned in the SAML response based on whether the user logs in as a service desk customer or a regular user. In the example below the user logged in as a regular user. 

{
  "nameId": "testuser",
  "attributes": {
    "groups": [
      "testgroup"
    ],
    "ATTR_SD_CUSTOMER": [
      "false"
    ],
    "ATTR_NAMEID": [
      "testuser"
    ]
  }
CODE


You can find example scripts below.

Examples

Login any user as guestuser if the attribute groups contains guests. 

This script should be mapped to the Application-attribute Username and assumes that the usernames comes from the SAML Name-ID:

// Check the group-attribute for the entry "guests"
if(mapping.groups.contains("guests")) {
    // Return the static value "guestuser"
   return "guestuser"
} else {
    // Otherwise use the value from the Name ID
    return ATTR_NAMEID
}
GROOVY

Set the last name to uppercase

This script should be applied to the Application-attribute Full Name and assumes the first name is in first and the last name in last

// If the attribute lastName is present transform it to uppercase,
// otherwise used an empty string
def lastName = mapping.lastName[0] ? mapping.lastName[0].toUpperCase() : ""

// Groovy GStrings allow variable substition with $variable or ${expression}
// toString() is required here because GStrings must be explicitly turned into Java-Strings.
def fullName = "${mapping.firstName[0]} $lastName".toString()

// The SLF4J-logger de.resolution.retransform.impl.transformers.groovy.GroovyTransformerScript
// is available as logger and can be used to write to the application-log.
// warn is enabled by default, so this message should be visible in the log 
logger.warn("########## This is the new full name: {}",fullName)

// Return the value
return fullName
CODE

Combine groups from attributes with the value true

In this example. the IdP sends a fixed set of group names as keys with the value true if the user is member of that group:

"attributes": {
    "grp1": [ "true"],
	"grp2": [ "true"],
	"grp3": [ "false"]
},
CODE
def groups = []

if(mapping.grp1?.contains("true")) {
    groups.add("grp1");
}
if(mapping.grp2?.contains("true")) {
    groups.add("grp2");
}
if(mapping.grp3?.contains("true")) {
    groups.add("grp3");
}

logger.warn("Groups are {}", groups)

return groups
CODE

Handle Groups Not Sent As Multivalue Attribute in SAML Response

def trafoMap = ["20368564" : "stash-users", "10096280" : "other-group"] // list of key/ value to replace group names after splitting
def splitted = mapping.Groups[0].split(",") // read "Groups" attribute from SAML Response split by comma
return splitted
	.collect{trafoMap[it]} // apply transformation rules from trafoMap (search and replace)
	.findAll{it} // filter null values a.k.a. drop groups not in the trafoMap
GROOVY

Transform one group from the SAML response to two or more groups

// Input your data as per the descriptions below
// Replace YourIDPGroupAttribute with your actual IdP Group Attribute
def idpGroupAttribute = "YourIDPGroupAttribute"

// Replace IdP_groupName with the group name that you need to transform
def idpGroupName = "IdP_groupName"

// Replace "replacement1" and "replacement2" with the actual group names replacements, and you can add other elements if needed
def replacements = ["replacement1", "replacement2"]

// No need to change anything in the following section
def groups = mapping.get(idpGroupAttribute)
if (groups.contains(idpGroupName)) {
    groups.remove(idpGroupName)
    groups.addAll(replacements)
} 
return groups
CODE

Transform one group from the SAML response to two or more groups and also perform more direct transformations

// Input your data as per the descriptions below
// Replace YourIDPGroupAttribute with your actual IdP Group Attribute
// The example below is the Azure AD Default groups claim
def idpGroupAttribute = "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"

// Replace IdP_groupName_1 with the group name that you want to transform into multiple other groups
def idpGroupName_1 = "my-group-1"

// Add as many groups as you want to be assigned to the user, if idpGroupName_1 is present
def idpGroupName_1_transform_to_groups = ["your-group-1", "your-group-2"]

// No need to change anything in the following block, this takes
def groups = mapping.get(idpGroupAttribute)
if (groups.contains(idpGroupName_1)) {
    groups.remove(idpGroupName_1)
    groups.addAll(idpGroupName_1_transform_to_groups)
} 

// Add more 1:1 replacements here
def idpGroupName_2 = "transform-me-1"
def idpGroupNameReplacement_2 = "your-group-3"

if (groups.contains(idpGroupName_2)) {
    groups.remove(idpGroupName_2)
    groups.add(idpGroupNameReplacement_2)
} 


def idpGroupName_3 = "transform-me-2"
def idpGroupNameReplacement_3 = "your-group-4"


if (groups.contains(idpGroupName_3)) {
    groups.remove(idpGroupName_3)
    groups.add(idpGroupNameReplacement_3)
} 

return groups
CODE

Allow user authentication based on the email domain of the user 

// Check the email-attribute and if it is not empty check if it contains the email domain
if (mapping.email[0] != null) {
	if (mapping.email[0].contains("@lab.resolution.de")) {
    	// if it contains the domain return the NameID
    	return mapping.ATTR_NAMEID
	} else {
    	// Otherwise drop the authentication
    	return DROP_ALL
	}
} else {
    // if email-attribute and if it is empty log a warning message and drop the authentication
	logger.warn("Dropping User authentication due to missing SAML attribute email")
	return DROP_ALL
}
CODE

Add a user into a specific group when not calling the JSM portal and the IdP returns a specific group

def groupsToReturn = (groups != null && groups != DROP) ? groups : []
//Check ATTR_SD_CUSTOMER attribute is false and if user is in group g1, when both conditions are met add the user to the group jira-servicedesk-users
if(ATTR_SD_CUSTOMER != ["true"]) {
    logger.warn("SD Portal is NOT called")
    if(groupsToReturn.contains("g1")) {
        logger.warn("User is in g1")
        groupsToReturn.add("jira-servicedesk-users")       
    } else {
		//If the user is not in group g1 
        logger.warn("User is not in g1")
    }
//If user has called the JSM portal    
} else {
	//If user has called the JSM portal    
	logger.warn("SD Portal is called")
}
return groupsToReturn

GROOVY


Migrate Groovy-Transformations from version 4.x

The way to write transformation-scripts has changed with SAML 5.0. "old-style"-script should still work, but we strongly recommend to update them to the new way.

Attributes are available without mapping-prefix

The attributes are accessible directly in the script's Binding, so e.g. instead of mapping.ATTR_NAMEID you can write ATTR_NAMEID

Return value instead of setting mapping.groovyResult

Instead of setting mapping.groovyResult = <valueToSet>, just return the value. It is no longer necessary to create a list for this value. 

Return DROP_ALL instead of setting mapping.drop = true

When a user should be dropped, return the predefined variable DROP_ALL instead of setting mapping.drop = true