From 5f05cd40743a827ebe809c4db5446dbfed3f17a8 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Fri, 23 Aug 2024 16:55:19 +0100 Subject: [PATCH 1/5] Add group management to LDAP SSO fixes #4002 --- forge/ee/lib/sso/index.js | 129 ++++++++++++++++++ .../admin/Settings/SSO/createEditProvider.vue | 88 +++++++++++- 2 files changed, 210 insertions(+), 7 deletions(-) diff --git a/forge/ee/lib/sso/index.js b/forge/ee/lib/sso/index.js index 95f12ec8ac..bbfed10bf0 100644 --- a/forge/ee/lib/sso/index.js +++ b/forge/ee/lib/sso/index.js @@ -262,6 +262,10 @@ module.exports.init = async function (app) { userClient = new Client(clientOptions) try { await userClient.bind(userDN, password) + // if ldap group support enabled + if (providerConfig.options.groupMapping) { + await updateTeamMembershipLDAP(adminClient, user, userDN, providerConfig.options) + } return true } catch (err) { // Failed to bind user @@ -424,6 +428,131 @@ module.exports.init = async function (app) { } } + // LDAP Group Membership + async function updateTeamMembershipLDAP (adminClient, user, userDN, providerOpts) { + const filter = `(|(uniqueMember=${userDN})(member=${userDN}))` + // const { searchEntries } = await adminClient.search(providerOpts.options.groupsDN, { + const { searchEntries } = await adminClient.search('ou=groups,dc=hardill,dc=me,dc=uk', { + filter, + attributes: ['cn'] + }) + const promises = [] + let adminGroup = false + const desiredTeamMemberships = {} + const groupRegEx = /^ff-(.+)-([^-]+)$/ + for (const i in searchEntries) { + + const match = groupRegEx.exec(searchEntries[i].cn) + if (match) { + app.log.debug(`Found group ${searchEntries[i].cn} for user ${user.username}`) + const teamSlug = match[1] + const teamRoleName = match[2] + const teamRole = Roles[teamRoleName] + // Check this role is a valid team role + if (TeamRoles.includes(teamRole)) { + // Check if this team is allowed to be managed for this SSO provider + // - either `groupAllTeams` is true (allowing all teams to be managed this way) + // - or `groupTeams` (array) contains the teamSlug + if (providerOpts.groupAllTeams || (providerOpts.groupTeams || []).includes(teamSlug)) { + // In case we have multiple assertions for a single team, + // ensure we keep the highest level of access + desiredTeamMemberships[teamSlug] = Math.max(desiredTeamMemberships[teamSlug] || 0, teamRole) + } + } + } + if (providerOpts.groupAdmin && providerOpts.groupAdminName === searchEntries[i].cn) { + adminGroup = true + } + } + if (providerOpts.groupAdmin) { + if (user.admin && !adminGroup) { + app.auditLog.User.user.updatedUser(0, null, [{ key: 'admin', old: true, new: false }], user) + user.admin = false + try { + await user.save() + } catch (err) { + // did we just fail remove the last admin? + app.log.info(`Failed to remove admin from ${user.username}, as this would have been the last admin`) + } + } else if (adminGroup && !user.admin) { + app.auditLog.User.user.updatedUser(0, null, [{ key: 'admin', old: false, new: true }], user) + user.admin = true + await user.save() + } + } + + // Get the existing memberships and generate a slug->membership object (existingMemberships) + const existingMemberships = {} + ;((await user.getTeamMemberships(true)) || []).forEach(membership => { + // Filter out any teams that are not to be managed by this configuration. + // A team is managed by this configuration if any of the follow is true: + // - groupAllTeams is true (all teams to be managed) + // - groupTeams includes this team (this is explicitly a team to be managed) + // - groupOtherTeams is false (not allowed to be a member of other teams - so need to remove them) + if ( + providerOpts.groupAllTeams || + (providerOpts.groupTeams || []).includes(membership.Team.slug) || + !providerOpts.groupOtherTeams + ) { + existingMemberships[membership.Team.slug] = membership + } + }) + // We now have the list of desiredTeamMemberships and existingMemberships + // that are in scope of being modified + + // - Check each existing membership + // - if in desired list, update role to match and delete from desired list + // - if not in desired list, + // - if groupOtherTeams is false or, delete membership + // - else leave alone + for (const [teamSlug, membership] of Object.entries(existingMemberships)) { + if (Object.hasOwn(desiredTeamMemberships, teamSlug)) { + // This team is in the desired list + if (desiredTeamMemberships[teamSlug] !== membership.role) { + // Role has changed - update membership + const updates = new app.auditLog.formatters.UpdatesCollection() + const oldRole = app.auditLog.formatters.roleObject(membership.role) + const role = app.auditLog.formatters.roleObject(desiredTeamMemberships[teamSlug]) + updates.push('role', oldRole.role, role.role) + membership.role = desiredTeamMemberships[teamSlug] + promises.push(membership.save().then(() => { + return app.auditLog.Team.team.user.roleChanged(user, null, membership.Team, user, updates) + })) + } else { + // Role has not changed - no update needed + // console.log(`no change needed for team ${teamSlug} role ${membership.role}`) + } + // Remove from the desired list as it has been dealt with + delete desiredTeamMemberships[teamSlug] + } else { + // console.log(`removing from team ${teamSlug}`) + // This team is not in the desired list - delete the membership + promises.push(membership.destroy().then(() => { + return app.auditLog.Team.team.user.removed(user, null, membership.Team, user) + })) + } + } + // - Check remaining desired memberships + // - create membership + for (const [teamSlug, teamRole] of Object.entries(desiredTeamMemberships)) { + // This is a new team membership + promises.push(app.db.models.Team.bySlug(teamSlug).then(team => { + if (team) { + // console.log(`adding to team ${teamSlug} role ${teamRole}`) + return app.db.controllers.Team.addUser(team, user, teamRole).then(() => { + return app.auditLog.Team.team.user.added(user, null, team, user) + }) + } else { + // console.log(`team not found ${teamSlug}`) + // Unrecognised team - ignore + return null + } + })) + } + + await Promise.all(promises) + } + return { handleLoginRequest, isSSOEnabledForEmail, diff --git a/frontend/src/pages/admin/Settings/SSO/createEditProvider.vue b/frontend/src/pages/admin/Settings/SSO/createEditProvider.vue index f4cee705c7..03b0bcb76c 100644 --- a/frontend/src/pages/admin/Settings/SSO/createEditProvider.vue +++ b/frontend/src/pages/admin/Settings/SSO/createEditProvider.vue @@ -43,7 +43,7 @@