How to: Map SSO Claims to User Groups in Content Hub
One of the most frequent questions I hear when starting a Content Hub implementation is whether it is possible to map SSO claims to user groups. As is so often the case with Content Hub, the answer is yes! This can be achieved using a user sign in script.
Script
The first thing we are going to do then is to create the script. This is done by going to Manage > Scripts and selecting “+ Script”. Enter a meaningful name and description, and as we want the script to execute whenever a user signs in, we must set the type as “User sign-in”. Once the script is created, click on it to open it, select “Edit” to open the script editor and copy the script below.
Important: If a user sign in script throws an error, this will prevent a user from signing in. It is important, therefore, that we surround our logic with a try…catch block to catch and deal with any exceptions.
using System.Linq;
using Stylelabs.M.Sdk;
using Stylelabs.M.Scripting.Types.V1_0.User;
using Stylelabs.M.Scripting.Types.V1_0.User.SignIn;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
const string ROLES_MAP_SETTING_CATEGORY = "NV.Dam.Settings";
const string ROLES_MAP_SETTING_NAME = "ExternalClaimRolesUserGroupMapping";
class GroupMapping {
public string Role {get; set;}
public string[] UserGroups {get; set;}
}
try {
await RunScriptAsync();
}
catch(Exception ex)
{
MClient.Logger.Error(ex);
}
public async Task RunScriptAsync() {
if(Context.AuthenticationSource == AuthenticationSource.Internal) {
MClient.Logger.Info($"Authentication source is Internal. Exiting.");
return;
}
MClient.Logger.Info($"Script called for user {Context.User.Id}");
MClient.Logger.Debug($"User claims: {string.Join(",\r\n", Context.ExternalUserInfo.Claims.Select(c => $"{c.Type}: {c.Value}"))}");
var roleMapSettings = await GetSettingValueAsync<JObject>(ROLES_MAP_SETTING_CATEGORY, ROLES_MAP_SETTING_NAME);
if(roleMapSettings == null) {
MClient.Logger.Error($"Could not load role map settings. Exiting.");
return;
}
var roleType = roleMapSettings.GetValue("ClaimRoleType").Value<string>();
var rolesMap = roleMapSettings.GetValue("ClaimRoleMap").ToObject<IEnumerable<GroupMapping>>();
var roleClaims = GetClaimValues(roleType);
if(!roleClaims.Any()) {
MClient.Logger.Info($"No claims of type {roleType}. Exiting.");
return;
}
MClient.Logger.Debug($"Found {roleClaims.Count()} claim(s). {string.Join(",\r\n", roleClaims)}");
var roleMaps = rolesMap.Where(m => roleClaims.Contains(m.Role));
if(!roleMaps.Any())
{
MClient.Logger.Info($"Could not find mapping for any of the claimed roles. Exiting.\r\n{string.Join(",\r\n ", roleClaims)}");
return;
}
MClient.Logger.Debug($"Found {roleMaps.Count()} role map(s). For roles\r\n{string.Join(", ", roleMaps.Select(rm => rm.Role))}");
var userGroupNames = roleMaps.SelectMany(rm => rm.UserGroups);
if(!userGroupNames.Any())
{
MClient.Logger.Warn($"Could not find any user groups for any of the claimed roles. Exiting.");
return;
}
MClient.Logger.Debug($"Found {userGroupNames.Count()} user group(s).\r\n{string.Join(", ", userGroupNames)}");
var groupIds = await GetIdsForGroupNamesAsync(userGroupNames);
await AddUserGroupsToUserAsync(groupIds);
}
#region functions
public IEnumerable<string> GetClaimValues(string type) {
return Context.ExternalUserInfo.Claims.Where(c => c.Type == type).Select(c => c.Value);
}
public async Task<T> GetSettingValueAsync<T>(string category, string name) {
var settingEntity = await MClient.Settings.GetSettingAsync(category, name).ConfigureAwait(false);
return settingEntity.GetPropertyValue<T>("M.Setting.Value");
}
public async Task<IEnumerable<long>> GetIdsForGroupNamesAsync(IEnumerable<string> groupNames) {
var query = Query.CreateQuery(entities =>
from e in entities
where e.DefinitionName == "Usergroup" && e.Property("GroupName").In(groupNames)
select e);
var queryResult = await MClient.Querying.QueryIdsAsync(query);
return queryResult.Items;
}
public async Task AddUserGroupsToUserAsync(IEnumerable<long> userGroupIds) {
var userGroupToUserRelation = await Context.User.GetRelationAsync<IChildToManyParentsRelation>("UserGroupToUser");
foreach(var groupId in userGroupIds.Where(gid => !userGroupToUserRelation.Parents.Contains(gid)))
{
userGroupToUserRelation.Parents.Add(groupId);
}
await MClient.Entities.SaveAsync(Context.User);
}
#endregion
Configuration
Next, we need to add some configuration in a custom setting so that the script knows what claims to map to which user groups. To do this, go to Manage > Settings and click “+ Setting”. Add a meaningful label such as “SSO User Group mappings”, enter a name and select a category (or create a new one). You must then go back and update the script on lines 10 and 11 to reflect the name and category entered here. Once the setting is created, select it to open the editor, and copy the following JSON.
{
"ClaimRoleType": "{Claim}",
"ClaimRoleMap": [
{
"role": "{RoleName}",
"usergroups": [
{UserGroupsToAdd}
]
}
]
}
Next update the JSON to reflect your particular configuration. ClaimRoleType should be set to the role type from your SSO provider that the script should look at and the ClaimRoleMap provides a mapping between these claims and the user groups you wish to be added any user that has that role. For example:
{
"ClaimRoleType": "http://schemas.xmlsoap.org/claims/Group",
"ClaimRoleMap": [
{
"role": "Employees - EMEA",
"usergroups": [
"M.Builtin.SitecoreDAM.Everyone"
]
},
{
"role": "TEST_ROLE",
"usergroups": [
"Superusers"
]
},
{
"role": "TEST_ROLE2",
"usergroups": []
}
]
}
The reason why user group names are used here, rather than IDs is to allow for the same script and configuration to be used across various environments (E.G. dev, QA, prod). In Content Hub, entity IDs are environment specific, so whilst a particular ID may relate to a user group in one environment, it may be used for something else in another.
Finally, return to the script you created and switch the toggle to enable it.
Summary
It is probably worth noting that this will not remove user groups from users that have had claims removed, although it would be possible with a few tweaks to the script - if anyone fancies making them, feel free to comment below.