diff --git a/docs/admin/sso/ldap.md b/docs/admin/sso/ldap.md index 6ec1829491..7b529d70b5 100644 --- a/docs/admin/sso/ldap.md +++ b/docs/admin/sso/ldap.md @@ -112,6 +112,9 @@ the groups in the LDAP Provider - rather than using the team's id. However, a te by a team owner. Doing so will break the link between the group and the team membership - so should only be done with care. +An optional prefix and suffix can be include in the group name to support LDAP providers that have existing naming policies. The SSO configuration can be configured with the lengths of these values so they will be stripped off before the group name is validated. + +For example, if an organisation requires all groups to begin with `acme-org-`, a prefix length of `9` can be set and the group `acme-org-ff-development-owner` will be handled as `ff-development-owner`. ## Managing Admin users The SSO Configuration can be configured to manage the admin users of the platform by enabling the diff --git a/docs/admin/sso/saml.md b/docs/admin/sso/saml.md index 3710341ed5..d55789d689 100644 --- a/docs/admin/sso/saml.md +++ b/docs/admin/sso/saml.md @@ -128,6 +128,9 @@ the groups in the SAML Provider - rather than using the team's id. However, a te by a team owner. Doing so will break the link between the group and the team membership - so should only be done with care. +An optional prefix and suffix can be include in the group name to support SAML providers that have existing naming policies. The SSO configuration can be configured with the lengths of these values so they will be stripped off before the group name is validated. + +For example, if an organisation requires all groups to begin with `acme-org-`, a prefix length of `9` can be set and the group `acme-org-ff-development-owner` will be handled as `ff-development-owner`. ## Managing Admin users The SSO Configuration can be configured to managed the admin users of the platform by enabling the diff --git a/forge/ee/lib/sso/index.js b/forge/ee/lib/sso/index.js index 381dd3b3a7..d4c39b7e9d 100644 --- a/forge/ee/lib/sso/index.js +++ b/forge/ee/lib/sso/index.js @@ -310,9 +310,17 @@ module.exports.init = async function (app) { const desiredTeamMemberships = {} app.log.debug(`SAML Group Assertions for ${user.username} ${JSON.stringify(groupAssertions)}`) groupAssertions.forEach(ga => { + // Trim prefix/postfix from group name + let shortGA = ga + if (providerOpts.groupPrefixLength || providerOpts.groupSuffixLength) { + const start = providerOpts.groupPrefixLength || 0 + const end = providerOpts.groupSuffixLength || 0 + shortGA = ga.slice(start, (end * -1)) + app.log.debug(`Converting Group name ${ga} to ${shortGA}`) + } // Parse the group name - format: 'ff-SLUG-ROLE' // Generate a slug->role object (desiredTeamMemberships) - const match = /^ff-(.+)-([^-]+)$/.exec(ga) + const match = /^ff-(.+)-([^-]+)$/.exec(shortGA) if (match) { const teamSlug = match[1] const teamRoleName = match[2] @@ -444,7 +452,14 @@ module.exports.init = async function (app) { const desiredTeamMemberships = {} const groupRegEx = /^ff-(.+)-([^-]+)$/ for (const i in searchEntries) { - const match = groupRegEx.exec(searchEntries[i].cn) + let shortCN = searchEntries[i].cn + if (providerOpts.groupPrefixLength || providerOpts.groupSuffixLength) { + // Trim prefix and postfix + const start = providerOpts.groupPrefixLength || 0 + const end = providerOpts.groupSuffixLength || 0 + shortCN = searchEntries[i].cn.slice(start, (end * -1)) + } + const match = groupRegEx.exec(shortCN) if (match) { app.log.debug(`Found group ${searchEntries[i].cn} for user ${user.username}`) const teamSlug = match[1] diff --git a/frontend/src/pages/admin/Settings/SSO/createEditProvider.vue b/frontend/src/pages/admin/Settings/SSO/createEditProvider.vue index 5060c47b64..e823d6cfe7 100644 --- a/frontend/src/pages/admin/Settings/SSO/createEditProvider.vue +++ b/frontend/src/pages/admin/Settings/SSO/createEditProvider.vue @@ -102,6 +102,14 @@ + + Group Name Prefix Length + + + + Group Name Suffix Length + + Team Scope @@ -164,7 +172,9 @@ export default { groupAssertionName: '', groupsDN: '', groupMapping: false, - groupAdminName: '' + groupAdminName: '', + groupPrefixLength: 0, + groupSuffixLength: 0 } }, errors: {}, @@ -182,7 +192,7 @@ export default { isGroupOptionsValid () { return !this.input.options.groupMapping || ( (this.input.type === 'saml' ? this.isGroupAssertionNameValid : this.isGroupsDNValid) && - this.isGroupAdminNameValid + this.isGroupAdminNameValid && this.isGroupPrefixValid && this.isGroupSuffixValid ) }, isGroupAssertionNameValid () { @@ -197,6 +207,18 @@ export default { groupsDNError () { return !this.isGroupsDNValid ? 'Group DN is required' : '' }, + groupPrefixLengthError () { + return this.input.options.groupPrefixLength < 0 ? 'Must be a greater or equal to 0' : '' + }, + isGroupPrefixValid () { + return this.input.options.groupPrefixLength >= 0 + }, + groupSuffixLengthError () { + return this.input.options.groupSuffixLength < 0 ? 'Must be a greater or equal to 0' : '' + }, + isGroupSuffixValid () { + return this.input.options.groupSuffixLength >= 0 + }, isGroupAdminNameValid () { return !this.input.options.groupAdmin || (this.input.options.groupAdminName && this.input.options.groupAdminName.length > 0) }, @@ -303,7 +325,9 @@ export default { groupOtherTeams: false, groupAdmin: false, groupAdminName: 'ff-admins', - groupAssertionName: 'ff-roles' + groupAssertionName: 'ff-roles', + groupPrefixLength: 0, + groupSuffixLength: 0 } } else { this.loading = true @@ -343,6 +367,12 @@ export default { this.input.options.tlsVerifyServer = true } } + if (this.provider.options.groupPrefixLength === undefined) { + this.input.options.groupPrefixLength = 0 + } + if (this.provider.options.groupSuffixLength === undefined) { + this.input.options.groupSuffixLength = 0 + } this.originalValues = JSON.stringify(this.input) }, async testProvider () { diff --git a/test/unit/forge/ee/lib/sso/index_spec.js b/test/unit/forge/ee/lib/sso/index_spec.js index be9ec89925..2452660caa 100644 --- a/test/unit/forge/ee/lib/sso/index_spec.js +++ b/test/unit/forge/ee/lib/sso/index_spec.js @@ -365,5 +365,29 @@ d }) ;(await app.db.models.TeamMember.getTeamMembership(app.user.id, teams.ATeam.id)).should.have.property('role', Roles.Owner) }) + it('strip prefix and suffix from SAML groups', async function () { + // This should remove ownership from Alice in Team A + + // Starting state: + // Alice owner ATeam + + // Expected result: + // Alice owner ATeam - unchanged + await app.sso.updateTeamMembership({ + 'ff-roles': [ + 'test_ff-ateam-magician_err', + 'test_ff-ateam-member_test2', + 'test_ff-bteam-owner_test2', + 'ff-ateam-admin_test2' + ] + }, app.user, { + groupAssertionName: 'ff-roles', + groupAllTeams: true, + groupPrefixLength: 5, + groupSuffixLength: 6 + }) + ;(await app.db.models.TeamMember.getTeamMembership(app.user.id, teams.ATeam.id)).should.have.property('role', Roles.Member) + ;(await app.db.models.TeamMember.getTeamMembership(app.user.id, teams.BTeam.id)).should.have.property('role', Roles.Owner) + }) }) })