Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for prefix/suffix to SSO GroupNames #4902

Merged
merged 8 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/admin/sso/ldap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/admin/sso/saml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions forge/ee/lib/sso/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
36 changes: 33 additions & 3 deletions frontend/src/pages/admin/Settings/SSO/createEditProvider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@
<template #description>The name of the base object to search for groups</template>
</FormRow>
</div>
<FormRow v-model="input.options.groupPrefixLength" :error="groupPrefixLengthError" type="number">
Group Name Prefix Length
<template #description>The length of any prefix added to the FlowFuse Group Name format</template>
</FormRow>
<FormRow v-model="input.options.groupSuffixLength" :error="groupSuffixLengthError" type="number">
Group Name Suffix Length
<template #description>The length of any suffix added to the FlowFuse Group Name format</template>
</FormRow>
<FormRow v-model="input.options.groupAllTeams" :options="[{ value:true, label: 'Apply to all teams' }, { value:false, label: 'Apply to selected teams' }]">
Team Scope
<template #description>Should this apply to all teams on the platform, or just a restricted list of teams</template>
Expand Down Expand Up @@ -164,7 +172,9 @@ export default {
groupAssertionName: '',
groupsDN: '',
groupMapping: false,
groupAdminName: ''
groupAdminName: '',
groupPrefixLength: 0,
groupSuffixLength: 0
}
},
errors: {},
Expand All @@ -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 () {
Expand All @@ -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)
},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 () {
Expand Down
24 changes: 24 additions & 0 deletions test/unit/forge/ee/lib/sso/index_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})
Loading