mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-03-12 01:59:34 -05:00
feat(auth): refactor group sync
This commit is contained in:
177
pkg/models/team_sync.go
Normal file
177
pkg/models/team_sync.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func SyncExternalTeamsForUser(s *xorm.Session, u *user.User, teams []*Team, issuer, teamNameSuffix string) (err error) {
|
||||
|
||||
if len(teams) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Find old teams for user through LDAP
|
||||
oldLdapTeams, err := FindAllExternalTeamIDsForUser(s, u.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Assign or create teams for the user
|
||||
externalTeamIDs, err := assignOrCreateUserToTeams(s, u, teams, issuer, teamNameSuffix)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove user from teams they're no longer a member of
|
||||
teamIDsToLeave := utils.NotIn(oldLdapTeams, externalTeamIDs)
|
||||
err = removeUserFromTeamsByIDs(s, u, teamIDsToLeave)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetTeamByExternalIDAndIssuer returns a team matching the given external_id
|
||||
// For oidc team creation oidcID and Name need to be set
|
||||
func GetTeamByExternalIDAndIssuer(s *xorm.Session, oidcID string, issuer string) (*Team, error) {
|
||||
team := &Team{}
|
||||
has, err := s.
|
||||
Table("teams").
|
||||
Where("external_id = ? AND issuer = ?", oidcID, issuer).
|
||||
Get(team)
|
||||
if !has || err != nil {
|
||||
return nil, ErrExternalTeamDoesNotExist{issuer, oidcID}
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func FindAllExternalTeamIDsForUser(s *xorm.Session, userID int64) (ts []int64, err error) {
|
||||
err = s.
|
||||
Table("team_members").
|
||||
Where("user_id = ? ", userID).
|
||||
Join("RIGHT", "teams", "teams.id = team_members.team_id").
|
||||
Where("teams.external_id != ? AND teams.external_id IS NOT NULL", "").
|
||||
Cols("teams.id").
|
||||
Find(&ts)
|
||||
return
|
||||
}
|
||||
|
||||
func assignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []*Team, issuer, teamNameSuffix string) (syncedTeamIDs []int64, err error) {
|
||||
if len(teamData) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have seen these teams before.
|
||||
// Find or create Teams and assign user as teammember.
|
||||
teams, err := getOrCreateTeamsByIssuer(s, teamData, u, issuer, teamNameSuffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
tm := &TeamMember{
|
||||
TeamID: team.ID,
|
||||
UserID: u.ID,
|
||||
Username: u.Username,
|
||||
}
|
||||
exists, _ := tm.MembershipExists(s)
|
||||
if !exists {
|
||||
err = tm.Create(s, u)
|
||||
if err != nil {
|
||||
log.Errorf("Could not assign user %s to team %s: %v", u.Username, team.Name, err)
|
||||
}
|
||||
}
|
||||
syncedTeamIDs = append(syncedTeamIDs, team.ID)
|
||||
}
|
||||
|
||||
return syncedTeamIDs, err
|
||||
}
|
||||
|
||||
func removeUserFromTeamsByIDs(s *xorm.Session, u *user.User, teamIDs []int64) (err error) {
|
||||
if len(teamIDs) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf("Removing team_member with user_id %v from team_ids %v", u.ID, teamIDs)
|
||||
_, err = s.
|
||||
In("team_id", teamIDs).
|
||||
And("user_id = ?", u.ID).
|
||||
Delete(&TeamMember{})
|
||||
return err
|
||||
}
|
||||
|
||||
// getOrCreateTeamsByIssuer returns a slice of teams which were generated from the external provider data.
|
||||
// If a team did not exist previously it is automatically created.
|
||||
func getOrCreateTeamsByIssuer(s *xorm.Session, teamData []*Team, u *user.User, issuer, teamNameSuffix string) (teams []*Team, err error) {
|
||||
teams = []*Team{}
|
||||
|
||||
for _, externalTeam := range teamData {
|
||||
t, err := GetTeamByExternalIDAndIssuer(s, externalTeam.ExternalID, issuer)
|
||||
if err != nil && !IsErrExternalTeamDoesNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err != nil && IsErrExternalTeamDoesNotExist(err) {
|
||||
log.Debugf("Team with external ID %s and name %s for issuer %s does not exist. Creating team...", externalTeam.ExternalID, externalTeam.Name, externalTeam.Issuer)
|
||||
newTeam, err := createExternalTeam(s, externalTeam, u, issuer, teamNameSuffix)
|
||||
if err != nil {
|
||||
return teams, err
|
||||
}
|
||||
teams = append(teams, newTeam)
|
||||
continue
|
||||
}
|
||||
|
||||
// Compare the name and update if it changed
|
||||
if t.Name != getExternalTeamName(externalTeam.Name, teamNameSuffix) {
|
||||
t.Name = getExternalTeamName(externalTeam.Name, teamNameSuffix)
|
||||
}
|
||||
|
||||
// Compare the description and update if it changed
|
||||
if t.Description != externalTeam.Description {
|
||||
t.Description = externalTeam.Description
|
||||
}
|
||||
|
||||
err = t.Update(s, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Team with external id %s and name %s for issuer %s already exists.", externalTeam.ExternalID, t.Name, externalTeam.Issuer)
|
||||
teams = append(teams, t)
|
||||
}
|
||||
|
||||
return teams, err
|
||||
}
|
||||
|
||||
func createExternalTeam(s *xorm.Session, teamData *Team, u *user.User, issuer, teamNameSuffix string) (team *Team, err error) {
|
||||
team = &Team{
|
||||
Name: getExternalTeamName(teamData.Name, teamNameSuffix),
|
||||
Description: teamData.Description,
|
||||
ExternalID: teamData.ExternalID,
|
||||
Issuer: issuer,
|
||||
}
|
||||
err = team.CreateNewTeam(s, u, false)
|
||||
return team, err
|
||||
}
|
||||
|
||||
func getExternalTeamName(name, suffix string) string {
|
||||
return name + " (" + suffix + ")"
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/web"
|
||||
@@ -131,31 +130,6 @@ func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetTeamByExternalIDAndIssuer returns a team matching the given external_id
|
||||
// For oidc team creation oidcID and Name need to be set
|
||||
func GetTeamByExternalIDAndIssuer(s *xorm.Session, oidcID string, issuer string) (*Team, error) {
|
||||
team := &Team{}
|
||||
has, err := s.
|
||||
Table("teams").
|
||||
Where("external_id = ? AND issuer = ?", oidcID, issuer).
|
||||
Get(team)
|
||||
if !has || err != nil {
|
||||
return nil, ErrExternalTeamDoesNotExist{issuer, oidcID}
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func FindAllExternalTeamIDsForUser(s *xorm.Session, userID int64) (ts []int64, err error) {
|
||||
err = s.
|
||||
Table("team_members").
|
||||
Where("user_id = ? ", userID).
|
||||
Join("RIGHT", "teams", "teams.id = team_members.team_id").
|
||||
Where("teams.external_id != ? AND teams.external_id IS NOT NULL", "").
|
||||
Cols("teams.id").
|
||||
Find(&ts)
|
||||
return
|
||||
}
|
||||
|
||||
func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
|
||||
|
||||
if len(teams) == 0 {
|
||||
|
||||
@@ -28,17 +28,11 @@ import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type team struct {
|
||||
Name string
|
||||
DN string
|
||||
Description string
|
||||
}
|
||||
|
||||
func InitializeLDAPConnection() {
|
||||
if !config.AuthLdapEnabled.GetBool() {
|
||||
return
|
||||
@@ -171,9 +165,11 @@ func AuthenticateUserInLDAP(s *xorm.Session, username, password string) (u *user
|
||||
}
|
||||
|
||||
u, err = getOrCreateLdapUser(s, sr.Entries[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO this should be unified with openid
|
||||
syncUserGroups(l, u, userdn)
|
||||
err = syncUserGroups(l, u, userdn)
|
||||
|
||||
return u, err
|
||||
}
|
||||
@@ -208,7 +204,7 @@ func getOrCreateLdapUser(s *xorm.Session, entry *ldap.Entry) (u *user.User, err
|
||||
return
|
||||
}
|
||||
|
||||
func syncUserGroups(l *ldap.Conn, u *user.User, userdn string) {
|
||||
func syncUserGroups(l *ldap.Conn, u *user.User, userdn string) (err error) {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
@@ -228,10 +224,10 @@ func syncUserGroups(l *ldap.Conn, u *user.User, userdn string) {
|
||||
sr, err := l.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Errorf("Error searching for LDAP groups: %v", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
var teams []*team
|
||||
var teams []*models.Team
|
||||
|
||||
for _, group := range sr.Entries {
|
||||
groupName := group.GetAttributeValue("cn")
|
||||
@@ -242,145 +238,24 @@ func syncUserGroups(l *ldap.Conn, u *user.User, userdn string) {
|
||||
|
||||
for _, member := range members {
|
||||
if member == userdn {
|
||||
teams = append(teams, &team{
|
||||
teams = append(teams, &models.Team{
|
||||
Name: groupName,
|
||||
DN: group.DN,
|
||||
ExternalID: group.DN,
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(teams) > 0 {
|
||||
// Find old teams for user through LDAP
|
||||
oldLdapTeams, err := models.FindAllExternalTeamIDsForUser(s, u.ID)
|
||||
if err != nil {
|
||||
log.Errorf("Error retrieving external team ids for user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Assign or create teams for the user
|
||||
ldapTeamIDs, err := assignOrCreateUserToTeams(s, u, teams)
|
||||
if err != nil {
|
||||
log.Errorf("Could not assign or create user to teams: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove user from teams they're no longer a member of
|
||||
teamIDsToLeave := utils.NotIn(oldLdapTeams, ldapTeamIDs)
|
||||
err = RemoveUserFromTeamsByIDs(s, u, teamIDsToLeave)
|
||||
if err != nil {
|
||||
log.Errorf("Error while removing user from teams: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Errorf("Error committing LDAP team changes: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []*team) (ldapTeamIDs []int64, err error) {
|
||||
if len(teamData) == 0 {
|
||||
err = models.SyncExternalTeamsForUser(s, u, teams, user.IssuerLDAP, "LDAP")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have seen these teams before.
|
||||
// Find or create Teams and assign user as teammember.
|
||||
teams, err := GetOrCreateTeamsByLDAP(s, teamData, u)
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
log.Errorf("Error verifying team for %v, got %v. Error: %v", u.Name, teams, err)
|
||||
return nil, err
|
||||
_ = s.Rollback()
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
tm := models.TeamMember{
|
||||
TeamID: team.ID,
|
||||
UserID: u.ID,
|
||||
Username: u.Username,
|
||||
}
|
||||
exists, _ := tm.MembershipExists(s)
|
||||
if !exists {
|
||||
err = tm.Create(s, u)
|
||||
if err != nil {
|
||||
log.Errorf("Could not assign user %s to team %s: %v", u.Username, team.Name, err)
|
||||
}
|
||||
}
|
||||
ldapTeamIDs = append(ldapTeamIDs, team.ID)
|
||||
}
|
||||
|
||||
return ldapTeamIDs, err
|
||||
}
|
||||
|
||||
func RemoveUserFromTeamsByIDs(s *xorm.Session, u *user.User, teamIDs []int64) (err error) {
|
||||
if len(teamIDs) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf("Removing team_member with user_id %v from team_ids %v", u.ID, teamIDs)
|
||||
_, err = s.
|
||||
In("team_id", teamIDs).
|
||||
And("user_id = ?", u.ID).
|
||||
Delete(&models.TeamMember{})
|
||||
return err
|
||||
}
|
||||
|
||||
func getLDAPTeamName(name string) string {
|
||||
return name + " (LDAP)"
|
||||
}
|
||||
|
||||
func createLDAPTeam(s *xorm.Session, teamData *team, u *user.User) (team *models.Team, err error) {
|
||||
team = &models.Team{
|
||||
Name: getLDAPTeamName(teamData.Name),
|
||||
Description: teamData.Description,
|
||||
ExternalID: teamData.DN,
|
||||
Issuer: user.IssuerLDAP,
|
||||
}
|
||||
err = team.CreateNewTeam(s, u, false)
|
||||
return team, err
|
||||
}
|
||||
|
||||
// GetOrCreateTeamsByLDAP returns a slice of teams which were generated from the LDAP data.
|
||||
// If a team did not exist previously it is automatically created.
|
||||
func GetOrCreateTeamsByLDAP(s *xorm.Session, teamData []*team, u *user.User) (teams []*models.Team, err error) {
|
||||
teams = []*models.Team{}
|
||||
|
||||
for _, ldapTeam := range teamData {
|
||||
t, err := models.GetTeamByExternalIDAndIssuer(s, ldapTeam.DN, user.IssuerLDAP)
|
||||
if err != nil && !models.IsErrExternalTeamDoesNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err != nil && models.IsErrExternalTeamDoesNotExist(err) {
|
||||
log.Debugf("Team with LDAP DN %v and name %v does not exist. Creating team...", ldapTeam.DN, ldapTeam.Name)
|
||||
newTeam, err := createLDAPTeam(s, ldapTeam, u)
|
||||
if err != nil {
|
||||
return teams, err
|
||||
}
|
||||
teams = append(teams, newTeam)
|
||||
continue
|
||||
}
|
||||
|
||||
// Compare the name and update if it changed
|
||||
if t.Name != getLDAPTeamName(ldapTeam.Name) {
|
||||
t.Name = getLDAPTeamName(ldapTeam.Name)
|
||||
}
|
||||
|
||||
// Compare the description and update if it changed
|
||||
if t.Description != ldapTeam.Description {
|
||||
t.Description = ldapTeam.Description
|
||||
}
|
||||
|
||||
err = t.Update(s, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Team with LDAP DN %v and name %v already exists.", ldapTeam.DN, t.Name)
|
||||
teams = append(teams, t)
|
||||
}
|
||||
|
||||
return teams, err
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user