diff --git a/pkg/models/team_sync.go b/pkg/models/team_sync.go new file mode 100644 index 000000000..2da70ea3e --- /dev/null +++ b/pkg/models/team_sync.go @@ -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 . + +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 + ")" +} diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 2adee5fd7..676bdef7b 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -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 { diff --git a/pkg/modules/auth/ldap/ldap.go b/pkg/modules/auth/ldap/ldap.go index 63e7f0a0a..79226320e 100644 --- a/pkg/modules/auth/ldap/ldap.go +++ b/pkg/modules/auth/ldap/ldap.go @@ -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 }