harbor/src/core/auth/ldap/ldap.go

270 lines
7.6 KiB
Go

// Copyright 2018 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ldap
import (
"fmt"
"regexp"
"strings"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/dao/group"
"github.com/goharbor/harbor/src/common/utils"
goldap "gopkg.in/ldap.v2"
"github.com/goharbor/harbor/src/common/models"
ldapUtils "github.com/goharbor/harbor/src/common/utils/ldap"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/auth"
"github.com/goharbor/harbor/src/core/config"
)
// Auth implements AuthenticateHelper interface to authenticate against LDAP
type Auth struct {
auth.DefaultAuthenticateHelper
}
// Authenticate checks user's credential against LDAP based on basedn template and LDAP URL,
// if the check is successful a dummy record will be inserted into DB, such that this user can
// be associated to other entities in the system.
func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
p := m.Principal
if len(strings.TrimSpace(p)) == 0 {
log.Debugf("LDAP authentication failed for empty user id.")
return nil, auth.NewErrAuth("Empty user id")
}
ldapSession, err := ldapUtils.LoadSystemLdapConfig()
if err != nil {
return nil, fmt.Errorf("can not load system ldap config: %v", err)
}
if err = ldapSession.Open(); err != nil {
log.Warningf("ldap connection fail: %v", err)
return nil, err
}
defer ldapSession.Close()
ldapUsers, err := ldapSession.SearchUser(p)
if err != nil {
log.Warningf("ldap search fail: %v", err)
return nil, err
}
if len(ldapUsers) == 0 {
log.Warningf("Not found an entry.")
return nil, auth.NewErrAuth("Not found an entry")
} else if len(ldapUsers) != 1 {
log.Warningf("Found more than one entry.")
return nil, auth.NewErrAuth("Multiple entries found")
}
log.Debugf("Found ldap user %+v", ldapUsers[0])
u := models.User{}
u.Username = ldapUsers[0].Username
u.Email = strings.TrimSpace(ldapUsers[0].Email)
u.Realname = ldapUsers[0].Realname
ugIDs := []int{}
dn := ldapUsers[0].DN
if err = ldapSession.Bind(dn, m.Password); err != nil {
log.Warningf("Failed to bind user, username: %s, dn: %s, error: %v", u.Username, dn, err)
return nil, auth.NewErrAuth(err.Error())
}
// Retrieve ldap related info in login to avoid too many traffic with LDAP server.
// Get group admin dn
groupCfg, err := config.LDAPGroupConf()
groupAdminDN := utils.TrimLower(groupCfg.LdapGroupAdminDN)
// Attach user group
for _, groupDN := range ldapUsers[0].GroupDNList {
groupDN = utils.TrimLower(groupDN)
// Attach LDAP group admin
if len(groupAdminDN) > 0 && groupAdminDN == groupDN {
u.HasAdminRole = true
}
userGroupQuery := models.UserGroup{
GroupType: 1,
LdapGroupDN: groupDN,
}
userGroups, err := group.QueryUserGroup(userGroupQuery)
if err != nil {
continue
}
if len(userGroups) == 0 {
continue
}
ugIDs = append(ugIDs, userGroups[0].ID)
}
u.GroupIDs = ugIDs
return &u, nil
}
// OnBoardUser will check if a user exists in user table, if not insert the user and
// put the id in the pointer of user model, if it does exist, return the user's profile.
func (l *Auth) OnBoardUser(u *models.User) error {
if u.Email == "" {
if strings.Contains(u.Username, "@") {
u.Email = u.Username
}
}
u.Password = "12345678AbC" // Password is not kept in local db
u.Comment = "from LDAP." // Source is from LDAP
return dao.OnBoardUser(u)
}
// SearchUser -- Search user in ldap
func (l *Auth) SearchUser(username string) (*models.User, error) {
var user models.User
ldapSession, err := ldapUtils.LoadSystemLdapConfig()
if err = ldapSession.Open(); err != nil {
return nil, fmt.Errorf("Failed to load system ldap config, %v", err)
}
ldapUsers, err := ldapSession.SearchUser(username)
if err != nil {
return nil, fmt.Errorf("Failed to search user in ldap")
}
if len(ldapUsers) > 1 {
log.Warningf("There are more than one user found, return the first user")
}
if len(ldapUsers) > 0 {
user.Username = strings.TrimSpace(ldapUsers[0].Username)
user.Realname = strings.TrimSpace(ldapUsers[0].Realname)
user.Email = strings.TrimSpace(ldapUsers[0].Email)
log.Debugf("Found ldap user %v", user)
} else {
return nil, fmt.Errorf("No user found, %v", username)
}
return &user, nil
}
// SearchGroup -- Search group in ldap authenticator, groupKey is LDAP group DN.
func (l *Auth) SearchGroup(groupKey string) (*models.UserGroup, error) {
if _, err := goldap.ParseDN(groupKey); err != nil {
return nil, auth.ErrInvalidLDAPGroupDN
}
ldapSession, err := ldapUtils.LoadSystemLdapConfig()
if err != nil {
return nil, fmt.Errorf("can not load system ldap config: %v", err)
}
if err = ldapSession.Open(); err != nil {
log.Warningf("ldap connection fail: %v", err)
return nil, err
}
defer ldapSession.Close()
userGroupList, err := ldapSession.SearchGroupByDN(groupKey)
if err != nil {
log.Warningf("ldap search group fail: %v", err)
return nil, err
}
if len(userGroupList) == 0 {
return nil, fmt.Errorf("Failed to searh ldap group with groupDN:%v", groupKey)
}
userGroup := models.UserGroup{
GroupName: userGroupList[0].GroupName,
LdapGroupDN: userGroupList[0].GroupDN,
}
return &userGroup, nil
}
// OnBoardGroup -- Create Group in harbor DB, if altGroupName is not empty, take the altGroupName as groupName in harbor DB.
func (l *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
if _, err := goldap.ParseDN(u.LdapGroupDN); err != nil {
return auth.ErrInvalidLDAPGroupDN
}
if len(altGroupName) > 0 {
u.GroupName = altGroupName
}
u.GroupType = common.LDAPGroupType
// Check duplicate LDAP DN in usergroup, if usergroup exist, return error
userGroupList, err := group.QueryUserGroup(models.UserGroup{LdapGroupDN: u.LdapGroupDN})
if err != nil {
return err
}
if len(userGroupList) > 0 {
return auth.ErrDuplicateLDAPGroup
}
return group.OnBoardUserGroup(u)
}
// PostAuthenticate -- If user exist in harbor DB, sync email address, if not exist, call OnBoardUser
func (l *Auth) PostAuthenticate(u *models.User) error {
exist, err := dao.UserExists(*u, "username")
if err != nil {
return err
}
if exist {
queryCondition := models.User{
Username: u.Username,
}
dbUser, err := dao.GetUser(queryCondition)
if err != nil {
return err
}
if dbUser == nil {
fmt.Printf("User not found in DB %+v", u)
return nil
}
u.UserID = dbUser.UserID
// If user has admin role already, do not overwrite by user info in DB.
u.HasAdminRole = u.HasAdminRole || dbUser.HasAdminRole
if dbUser.Email != u.Email {
Re := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
if !Re.MatchString(u.Email) {
log.Debugf("Not a valid email address: %v, skip to sync", u.Email)
} else {
if err = dao.ChangeUserProfile(*u, "Email"); err != nil {
u.Email = dbUser.Email
log.Errorf("failed to sync user email: %v", err)
}
}
}
return nil
}
err = auth.OnBoardUser(u)
if err != nil {
return err
}
if u.UserID <= 0 {
return fmt.Errorf("Can not OnBoardUser %v", u)
}
return nil
}
func init() {
auth.Register("ldap_auth", &Auth{})
}