Grouper provisioning SCIM for AWS
This is for v4+
https://www.rfc-editor.org/rfc/rfc7643.html#section-4.1
AWS cannot retrieve a group's members, nor the groups a user belongs to, so Grouper cannot read membership state back from AWS (i.e. you cannot select memberships from AWS). Because Grouper cannot diff against the target, you choose how it drives memberships. Either strategy below works with AWS; pick based on the tradeoff.
Incremental patches (insert/delete) - leave
replaceMembershipsunset and drive memberships withinsertMemberships/deleteMemberships. Grouper computes add/remove deltas from its own sync tables and sends only the changes. Lighter weight and scales well for large groups, since it never re-sends a full member list. Tradeoff: it trusts Grouper's sync state, so if AWS drifts from that state (for example someone edits a membership directly in AWS) it will not self-correct until a full sync re-establishes the sync state.Replace memberships - set
replaceMemberships = truealong withcustomizeMembershipCrud = trueandrecalculateAllOperations = true. Grouper sends the entire desired member list for each group on every sync. Self-correcting, since it re-asserts the full membership even if state has drifted, but heavier, since it pushes every member of every group on each run. NoterecalculateAllOperations = trueis required here, otherwise Grouper computes only the delta against its sync table and the replace payload is incomplete.
Grouper is normally the system of record and memberships are not edited on the AWS side, so incremental patches are usually the better default; the Penn production AWS provisioners run this way. Use replace when you want each sync to re-assert the full membership regardless of drift.
External System
Grouper uses bearer token authentication to connect with SCIM V2 APIs. Create an external system like below.
We have tested SCIM integration for AWS, Github, Atlassian, Robin, and others. Even though they all follow SCIM, there are still many differences, so when you configure a SCIM provisioner, we ask for SCIM type. Based on the SCIM type, the provisioner framework can run extra validations to make integration more robust.
Note: for AWS (as of 7/30/2024) in the Group section of the config, you must set "include active on group create" to false
AWS SCIM Provisioning
Group fields and attributes - example request to create a group https://docs.aws.amazon.com/singlesignon/latest/developerguide/creategroup.html
Grouper name | Attribute or field | Type | Required? | Description |
|---|---|---|---|---|
id | field | String | required | UUID read from AWS. Select only. |
displayName | field | String | required | Display Name of the group in AWS. |
Entity fields and attributes - example request to create a user https://docs.aws.amazon.com/singlesignon/latest/developerguide/createuser.html
Make sure patch name strategy is "qualified"
Grouper name | Attribute or field | Type | Required? | Description |
|---|---|---|---|---|
id | field | String | required | UUID read from AWS. Select only. |
userName | attribute | String | required | User name |
displayName | attribute | String | required | Display name of the user |
familyName | attribute | String | required | Family name (Last name) |
givenName | attribute | String | required | Given name (First name) |
externalId | attribute | String | optional | External id |
formattedName | attribute | String | optional | Formatted name e.g Mr. John Smith, II |
middleName | attribute | String | optional | Middle name |
emailValue | attribute | String | optional | Email value e.g. test@example.com |
emailType | attribute | String | optional | Email type e.g. work |
userType | attribute | String | optional | User type e.g. Employee |
employeeNumber | attribute | String | optional | Employee number |
costCenter | attribute | String | optional | Cost center |
Configure SCIM settings in AWS for development purposes
Go to AWS Single Sign-On in the AWS management console, click IAM Identity Center on left
On the left, click on Settings
Change Identity source to External Identity Provider
Put a random valid URL in IdP Sign-in URL e.g https://grouperdemo.internet2.edu
Put a random valid URL in IdP issuer URL e.g https://grouperdemo.internet2.edu
Upload the cert for that URL
Enabled automatic provisioning
Get the endpoint and token
Github SCIM Provisioning
Github only supports SCIM for user operations. An organization must already exist for which members need to be managed. If you want to manage memberships of multiple organizations, configure a separate external system for each organization.
User fields and attributes
Grouper name | Attribute or field | Type | Required? | Description |
|---|---|---|---|---|
id | field | String | required | UUID read from Github. Select only. |
userName | attribute | String | required | User name |
displayName | attribute | String | optional | Display name of the user |
familyName | attribute | String | required | Family name (Last name) |
givenName | attribute | String | required | Given name (First name) |
externalId | attribute | String | optional | External id |
formattedName | attribute | String | optional | Formatted name e.g Mr. John Smith, II |
emailValue | attribute | String | required | Email value e.g. test@example.com |
emailType | attribute | String | optional | Email type e.g. work |
Configure SCIM settings in Github for development purposes
Go to Settings → Develop settings → Personal access tokens.
Generate a new token and keep it safe. You will need it when configuring the external system.
In your Github organization, you need SAML. For our testing we set up SAML integration between Github and Onelogin. You will need to set up an account on Onelogin. Github and Onelogin both offer trial versions for a few days.
The SCIM URL that you need to enter while configuring the external system would look like: https://api.github.com/scim/v2/organizations/yourOrgName/
Here is a video that shows how to integrate Onelogin with the Github organization (though you should integrate with your own saml).
Example provisioner config
provisioner.awsIdentityCenterIscProd.addDisabledFullSyncDaemon = true
provisioner.awsIdentityCenterIscProd.addDisabledIncrementalSyncDaemon = true
provisioner.awsIdentityCenterIscProd.bearerTokenExternalSystemConfigId = awsIdentityCenterIscProd
provisioner.awsIdentityCenterIscProd.class = edu.internet2.middleware.grouper.app.scim2Provisioning.GrouperScim2Provisioner
provisioner.awsIdentityCenterIscProd.customizeMembershipCrud = true
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache0entityAttribute = id
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache0has = true
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache0source = target
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache0type = entityAttribute
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache1entityAttribute = userName
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache1has = true
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache1source = target
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache1type = entityAttribute
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache2has = true
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache2source = grouper
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache2translationScript = \u0024{subject.getAttributeValue('last_name')}
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache2type = subjectTranslationScript
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache3has = true
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache3source = grouper
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache3translationScript = \u0024{subject.getAttributeValue('first_name')}
provisioner.awsIdentityCenterIscProd.entityAttributeValueCache3type = subjectTranslationScript
provisioner.awsIdentityCenterIscProd.entityAttributeValueCacheHas = true
provisioner.awsIdentityCenterIscProd.entityMatchingAttribute0name = userName
provisioner.awsIdentityCenterIscProd.entityMatchingAttribute1name = id
provisioner.awsIdentityCenterIscProd.entityMatchingAttributeCount = 2
provisioner.awsIdentityCenterIscProd.groupAllowedToAssign = penn:isc:nandt:services:aws:etc:awsIdentityCenterIscProdProvisioningAdmins
provisioner.awsIdentityCenterIscProd.groupAttributeValueCache0groupAttribute = id
provisioner.awsIdentityCenterIscProd.groupAttributeValueCache0has = true
provisioner.awsIdentityCenterIscProd.groupAttributeValueCache0source = target
provisioner.awsIdentityCenterIscProd.groupAttributeValueCache0type = groupAttribute
provisioner.awsIdentityCenterIscProd.groupAttributeValueCache1groupAttribute = displayName
provisioner.awsIdentityCenterIscProd.groupAttributeValueCache1has = true
provisioner.awsIdentityCenterIscProd.groupAttributeValueCache1source = target
provisioner.awsIdentityCenterIscProd.groupAttributeValueCache1type = groupAttribute
provisioner.awsIdentityCenterIscProd.groupAttributeValueCacheHas = true
provisioner.awsIdentityCenterIscProd.groupMatchingAttribute0name = displayName
provisioner.awsIdentityCenterIscProd.groupMatchingAttribute1name = id
provisioner.awsIdentityCenterIscProd.groupMatchingAttributeCount = 2
provisioner.awsIdentityCenterIscProd.hasTargetEntityLink = true
provisioner.awsIdentityCenterIscProd.hasTargetGroupLink = true
provisioner.awsIdentityCenterIscProd.insertMemberships = true
provisioner.awsIdentityCenterIscProd.logAllObjectsVerbose = true
provisioner.awsIdentityCenterIscProd.logAllObjectsVerboseToLogFile = false
provisioner.awsIdentityCenterIscProd.makeChangesToEntities = true
provisioner.awsIdentityCenterIscProd.numberOfEntityAttributes = 6
provisioner.awsIdentityCenterIscProd.numberOfGroupAttributes = 2
provisioner.awsIdentityCenterIscProd.operateOnGrouperEntities = true
provisioner.awsIdentityCenterIscProd.operateOnGrouperGroups = true
provisioner.awsIdentityCenterIscProd.operateOnGrouperMemberships = true
provisioner.awsIdentityCenterIscProd.provisioningType = membershipObjects
provisioner.awsIdentityCenterIscProd.removeAccentedChars = true
provisioner.awsIdentityCenterIscProd.scimNamePatchStrategy = qualified
provisioner.awsIdentityCenterIscProd.scimType = AWS
provisioner.awsIdentityCenterIscProd.selectAllEntities = true
provisioner.awsIdentityCenterIscProd.selectAllGroups = true
provisioner.awsIdentityCenterIscProd.selectMemberships = false
provisioner.awsIdentityCenterIscProd.showAdvanced = true
provisioner.awsIdentityCenterIscProd.showAssigningProvisioning = true
provisioner.awsIdentityCenterIscProd.startWith = this is start with read only
provisioner.awsIdentityCenterIscProd.subjectSourcesToProvision = pennperson
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.0.insert = false
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.0.name = id
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.0.showAdvancedAttribute = true
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.0.showAttributeCrud = true
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.0.update = false
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.1.name = emailValue
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.1.translateExpressionType = grouperProvisioningEntityField
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.1.translateFromGrouperProvisioningEntityField = subjectIdentifier1
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.2.name = familyName
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.2.translateExpressionType = grouperProvisioningEntityField
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.2.translateFromGrouperProvisioningEntityField = entityAttributeValueCache2
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.3.name = givenName
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.3.translateExpressionType = grouperProvisioningEntityField
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.3.translateFromGrouperProvisioningEntityField = entityAttributeValueCache3
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.4.name = userName
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.4.translateExpressionType = grouperProvisioningEntityField
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.4.translateFromGrouperProvisioningEntityField = subjectIdentifier1
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.5.name = displayName
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.5.translateExpressionType = grouperProvisioningEntityField
provisioner.awsIdentityCenterIscProd.targetEntityAttribute.5.translateFromGrouperProvisioningEntityField = name
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.0.insert = false
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.0.name = id
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.0.showAdvancedAttribute = true
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.0.showAttributeCrud = true
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.0.update = false
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.1.name = displayName
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.1.nullChecksInScript = true
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.1.translateExpressionType = translationScript
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.1.translateExpression = \u0024{var extensions = grouperProvisioningGroup.name.split(":"); extensions.get(size(extensions)-2) + '_' + extensions.get(size(extensions)-1);}
provisioner.awsIdentityCenterIscProd.targetGroupAttribute.1.translationContinueCondition = \u0024{grouperProvisioningGroup.name != null}