forked from casdoor/casdoor
909 lines
24 KiB
Go
909 lines
24 KiB
Go
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
|
//
|
|
// 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 object
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/casdoor/casdoor/conf"
|
|
"github.com/casdoor/casdoor/i18n"
|
|
"github.com/casdoor/casdoor/util"
|
|
goldap "github.com/go-ldap/ldap/v3"
|
|
"github.com/nyaruka/phonenumbers"
|
|
"github.com/thanhpk/randstr"
|
|
"golang.org/x/text/encoding/unicode"
|
|
)
|
|
|
|
// formatUserPhone processes phone number for a user based on their CountryCode
|
|
func formatUserPhone(u *User) {
|
|
if u.Phone == "" {
|
|
return
|
|
}
|
|
|
|
// 1. Normalize hint (e.g., "China" -> "CN") for the parser
|
|
countryHint := u.CountryCode
|
|
if strings.EqualFold(countryHint, "China") {
|
|
countryHint = "CN"
|
|
}
|
|
if len(countryHint) != 2 {
|
|
countryHint = "" // Only 2-letter codes are valid hints
|
|
}
|
|
|
|
// 2. Try parsing (Strictly using countryHint from LDAP)
|
|
num, err := phonenumbers.Parse(u.Phone, countryHint)
|
|
|
|
if err == nil && num != nil && phonenumbers.IsValidNumber(num) {
|
|
// Store a clean national number (digits only, without country prefix)
|
|
u.Phone = fmt.Sprint(num.GetNationalNumber())
|
|
}
|
|
}
|
|
|
|
type LdapConn struct {
|
|
Conn *goldap.Conn
|
|
IsAD bool
|
|
}
|
|
|
|
//type ldapGroup struct {
|
|
// GidNumber string
|
|
// Cn string
|
|
//}
|
|
|
|
type LdapUser struct {
|
|
UidNumber string `json:"uidNumber"`
|
|
Uid string `json:"uid"`
|
|
Cn string `json:"cn"`
|
|
GidNumber string `json:"gidNumber"`
|
|
// Gcn string
|
|
Uuid string `json:"uuid"`
|
|
UserPrincipalName string `json:"userPrincipalName"`
|
|
DisplayName string `json:"displayName"`
|
|
Mail string
|
|
Email string `json:"email"`
|
|
EmailAddress string
|
|
TelephoneNumber string
|
|
Mobile string `json:"mobile"`
|
|
MobileTelephoneNumber string
|
|
RegisteredAddress string
|
|
PostalAddress string
|
|
Country string `json:"country"`
|
|
CountryName string `json:"countryName"`
|
|
|
|
GroupId string `json:"groupId"`
|
|
Address string `json:"address"`
|
|
MemberOf []string `json:"memberOf"`
|
|
Attributes map[string]string `json:"attributes"`
|
|
}
|
|
|
|
type LdapGroup struct {
|
|
Dn string `json:"dn"`
|
|
Cn string `json:"cn"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Member []string `json:"member"`
|
|
ParentDn string `json:"parentDn"`
|
|
}
|
|
|
|
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
|
|
var conn *goldap.Conn
|
|
tlsConfig := tls.Config{
|
|
InsecureSkipVerify: ldap.AllowSelfSignedCert,
|
|
}
|
|
if ldap.EnableSsl {
|
|
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), &tlsConfig)
|
|
} else {
|
|
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = conn.Bind(ldap.Username, ldap.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isAD, err := isMicrosoftAD(conn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &LdapConn{Conn: conn, IsAD: isAD}, nil
|
|
}
|
|
|
|
func (l *LdapConn) Close() {
|
|
if l.Conn == nil {
|
|
return
|
|
}
|
|
|
|
err := l.Conn.Unbind()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
|
|
SearchFilter := "(objectClass=*)"
|
|
SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"}
|
|
|
|
searchReq := goldap.NewSearchRequest("",
|
|
goldap.ScopeBaseObject, goldap.NeverDerefAliases, 0, 0, false,
|
|
SearchFilter, SearchAttributes, nil)
|
|
searchResult, err := Conn.Search(searchReq)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if len(searchResult.Entries) == 0 {
|
|
return false, nil
|
|
}
|
|
isMicrosoft := false
|
|
|
|
type ldapServerType struct {
|
|
Vendorname string
|
|
Vendorversion string
|
|
IsGlobalCatalogReady string
|
|
ForestFunctionality string
|
|
}
|
|
var ldapServerTypes ldapServerType
|
|
for _, entry := range searchResult.Entries {
|
|
for _, attribute := range entry.Attributes {
|
|
switch attribute.Name {
|
|
case "vendorname":
|
|
ldapServerTypes.Vendorname = attribute.Values[0]
|
|
case "vendorversion":
|
|
ldapServerTypes.Vendorversion = attribute.Values[0]
|
|
case "isGlobalCatalogReady":
|
|
ldapServerTypes.IsGlobalCatalogReady = attribute.Values[0]
|
|
case "forestFunctionality":
|
|
ldapServerTypes.ForestFunctionality = attribute.Values[0]
|
|
}
|
|
}
|
|
}
|
|
if ldapServerTypes.Vendorname == "" &&
|
|
ldapServerTypes.Vendorversion == "" &&
|
|
ldapServerTypes.IsGlobalCatalogReady == "TRUE" &&
|
|
ldapServerTypes.ForestFunctionality != "" {
|
|
isMicrosoft = true
|
|
}
|
|
return isMicrosoft, err
|
|
}
|
|
|
|
func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
|
|
SearchAttributes := []string{
|
|
"uidNumber", "cn", "sn", "gidNumber", "entryUUID", "displayName", "mail", "email",
|
|
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress",
|
|
"c", "co", "memberOf",
|
|
}
|
|
if l.IsAD {
|
|
SearchAttributes = append(SearchAttributes, "sAMAccountName")
|
|
} else {
|
|
SearchAttributes = append(SearchAttributes, "uid")
|
|
}
|
|
|
|
for attribute := range ldapServer.CustomAttributes {
|
|
SearchAttributes = append(SearchAttributes, attribute)
|
|
}
|
|
|
|
// Some LDAP servers/configs use "{}" as a placeholder (e.g. "(uid={})").
|
|
// Casdoor doesn't interpolate it. For listing users, interpret it as a wildcard.
|
|
filter := strings.TrimSpace(ldapServer.Filter)
|
|
if filter == "" {
|
|
filter = "(objectClass=*)"
|
|
} else if strings.Contains(filter, "{}") {
|
|
filter = strings.ReplaceAll(filter, "{}", "*")
|
|
}
|
|
|
|
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
|
|
0, 0, false,
|
|
filter, SearchAttributes, nil)
|
|
searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(searchResult.Entries) == 0 {
|
|
return []LdapUser{}, nil
|
|
}
|
|
|
|
var ldapUsers []LdapUser
|
|
for _, entry := range searchResult.Entries {
|
|
var user LdapUser
|
|
for _, attribute := range entry.Attributes {
|
|
switch attribute.Name {
|
|
case "uidNumber":
|
|
user.UidNumber = attribute.Values[0]
|
|
case "uid":
|
|
user.Uid = attribute.Values[0]
|
|
case "sAMAccountName":
|
|
user.Uid = attribute.Values[0]
|
|
case "cn":
|
|
user.Cn = attribute.Values[0]
|
|
case "gidNumber":
|
|
user.GidNumber = attribute.Values[0]
|
|
case "entryUUID":
|
|
user.Uuid = attribute.Values[0]
|
|
case "objectGUID":
|
|
user.Uuid = attribute.Values[0]
|
|
case "userPrincipalName":
|
|
user.UserPrincipalName = attribute.Values[0]
|
|
case "displayName":
|
|
user.DisplayName = attribute.Values[0]
|
|
case "mail":
|
|
user.Mail = attribute.Values[0]
|
|
case "email":
|
|
user.Email = attribute.Values[0]
|
|
case "emailAddress":
|
|
user.EmailAddress = attribute.Values[0]
|
|
case "telephoneNumber":
|
|
user.TelephoneNumber = attribute.Values[0]
|
|
case "mobile":
|
|
user.Mobile = attribute.Values[0]
|
|
case "mobileTelephoneNumber":
|
|
user.MobileTelephoneNumber = attribute.Values[0]
|
|
case "registeredAddress":
|
|
user.RegisteredAddress = attribute.Values[0]
|
|
case "postalAddress":
|
|
user.PostalAddress = attribute.Values[0]
|
|
case "c":
|
|
user.Country = attribute.Values[0]
|
|
case "co":
|
|
user.CountryName = attribute.Values[0]
|
|
case "memberOf":
|
|
user.MemberOf = attribute.Values
|
|
default:
|
|
if propName, ok := ldapServer.CustomAttributes[attribute.Name]; ok {
|
|
if user.Attributes == nil {
|
|
user.Attributes = make(map[string]string)
|
|
}
|
|
user.Attributes[propName] = attribute.Values[0]
|
|
}
|
|
}
|
|
}
|
|
ldapUsers = append(ldapUsers, user)
|
|
}
|
|
|
|
return ldapUsers, nil
|
|
}
|
|
|
|
// GetLdapGroups fetches LDAP groups and organizational units
|
|
func (l *LdapConn) GetLdapGroups(ldapServer *Ldap) ([]LdapGroup, error) {
|
|
var allGroups []LdapGroup
|
|
|
|
// Search for LDAP groups (groupOfNames, groupOfUniqueNames, posixGroup)
|
|
groupFilters := []string{
|
|
"(objectClass=groupOfNames)",
|
|
"(objectClass=groupOfUniqueNames)",
|
|
"(objectClass=posixGroup)",
|
|
}
|
|
|
|
// Add Active Directory group filter
|
|
if l.IsAD {
|
|
groupFilters = append(groupFilters, "(objectClass=group)")
|
|
}
|
|
|
|
// Build combined filter
|
|
var filterBuilder strings.Builder
|
|
filterBuilder.WriteString("(|")
|
|
for _, filter := range groupFilters {
|
|
filterBuilder.WriteString(filter)
|
|
}
|
|
filterBuilder.WriteString(")")
|
|
|
|
SearchAttributes := []string{"cn", "name", "description", "member", "uniqueMember", "memberUid"}
|
|
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn,
|
|
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
|
|
filterBuilder.String(), SearchAttributes, nil)
|
|
|
|
searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
|
|
if err != nil {
|
|
// Groups might not exist, which is okay
|
|
return allGroups, nil
|
|
}
|
|
|
|
for _, entry := range searchResult.Entries {
|
|
group := LdapGroup{
|
|
Dn: entry.DN,
|
|
}
|
|
|
|
for _, attribute := range entry.Attributes {
|
|
switch attribute.Name {
|
|
case "cn":
|
|
group.Cn = attribute.Values[0]
|
|
case "name":
|
|
group.Name = attribute.Values[0]
|
|
case "description":
|
|
if len(attribute.Values) > 0 {
|
|
group.Description = attribute.Values[0]
|
|
}
|
|
case "member", "uniqueMember", "memberUid":
|
|
group.Member = append(group.Member, attribute.Values...)
|
|
}
|
|
}
|
|
|
|
// Use cn as name if name is not set
|
|
if group.Name == "" {
|
|
group.Name = group.Cn
|
|
}
|
|
|
|
// Parse parent DN from the entry DN
|
|
group.ParentDn = getParentDn(entry.DN)
|
|
|
|
allGroups = append(allGroups, group)
|
|
}
|
|
|
|
// Also fetch organizational units as groups
|
|
ouFilter := "(objectClass=organizationalUnit)"
|
|
ouSearchReq := goldap.NewSearchRequest(ldapServer.BaseDn,
|
|
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
|
|
ouFilter, []string{"ou", "description"}, nil)
|
|
|
|
ouSearchResult, err := l.Conn.SearchWithPaging(ouSearchReq, 100)
|
|
if err == nil {
|
|
for _, entry := range ouSearchResult.Entries {
|
|
ou := LdapGroup{
|
|
Dn: entry.DN,
|
|
}
|
|
|
|
for _, attribute := range entry.Attributes {
|
|
switch attribute.Name {
|
|
case "ou":
|
|
ou.Name = attribute.Values[0]
|
|
ou.Cn = attribute.Values[0]
|
|
case "description":
|
|
if len(attribute.Values) > 0 {
|
|
ou.Description = attribute.Values[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse parent DN from the entry DN
|
|
ou.ParentDn = getParentDn(entry.DN)
|
|
|
|
allGroups = append(allGroups, ou)
|
|
}
|
|
}
|
|
|
|
return allGroups, nil
|
|
}
|
|
|
|
// getParentDn extracts the parent DN from a full DN
|
|
func getParentDn(dn string) string {
|
|
// Split DN by comma
|
|
parts := strings.Split(dn, ",")
|
|
if len(parts) <= 1 {
|
|
return ""
|
|
}
|
|
|
|
// Remove the first component (the current node) and rejoin
|
|
return strings.Join(parts[1:], ",")
|
|
}
|
|
|
|
// parseDnToGroupName converts a DN to a group name
|
|
func parseDnToGroupName(dn string) string {
|
|
// Extract the CN or OU from the DN
|
|
parts := strings.Split(dn, ",")
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
|
|
firstPart := parts[0]
|
|
// Extract value after = sign
|
|
if idx := strings.Index(firstPart, "="); idx != -1 {
|
|
return firstPart[idx+1:]
|
|
}
|
|
|
|
return firstPart
|
|
}
|
|
|
|
func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
|
|
res := make([]LdapUser, len(users))
|
|
for i, user := range users {
|
|
res[i] = LdapUser{
|
|
UidNumber: user.UidNumber,
|
|
Uid: user.Uid,
|
|
Cn: user.Cn,
|
|
GroupId: user.GidNumber,
|
|
Uuid: user.GetLdapUuid(),
|
|
DisplayName: user.DisplayName,
|
|
Email: util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
|
|
Mobile: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
|
|
Address: util.ReturnAnyNotEmpty(user.Address, user.PostalAddress, user.RegisteredAddress),
|
|
Country: util.ReturnAnyNotEmpty(user.Country, user.CountryName),
|
|
CountryName: user.CountryName,
|
|
MemberOf: user.MemberOf,
|
|
Attributes: user.Attributes,
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUsers []LdapUser, failedUsers []LdapUser, err error) {
|
|
var uuids []string
|
|
for _, user := range syncUsers {
|
|
uuids = append(uuids, user.Uuid)
|
|
}
|
|
|
|
organization, err := getOrganization("admin", owner)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ldap, err := GetLdap(ldapId)
|
|
|
|
var dc []string
|
|
for _, basedn := range strings.Split(ldap.BaseDn, ",") {
|
|
if strings.Contains(basedn, "dc=") {
|
|
dc = append(dc, basedn[3:])
|
|
}
|
|
}
|
|
affiliation := strings.Join(dc, ".")
|
|
|
|
var ou []string
|
|
for _, admin := range strings.Split(ldap.Username, ",") {
|
|
if strings.Contains(admin, "ou=") {
|
|
ou = append(ou, admin[3:])
|
|
}
|
|
}
|
|
tag := strings.Join(ou, ".")
|
|
|
|
existUuids, err := GetExistUuids(owner, uuids)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
existUuidSet := make(map[string]struct{}, len(existUuids))
|
|
for _, uuid := range existUuids {
|
|
existUuidSet[uuid] = struct{}{}
|
|
}
|
|
|
|
for _, syncUser := range syncUsers {
|
|
_, found := existUuidSet[syncUser.Uuid]
|
|
if found {
|
|
existUsers = append(existUsers, syncUser)
|
|
}
|
|
|
|
if !found {
|
|
score, err := organization.GetInitScore()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
name, err := syncUser.buildLdapUserName(owner)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
newUser := &User{
|
|
Owner: owner,
|
|
Name: name,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
DisplayName: syncUser.buildLdapDisplayName(),
|
|
SignupApplication: organization.DefaultApplication,
|
|
Type: "normal-user",
|
|
Avatar: organization.DefaultAvatar,
|
|
Email: syncUser.Email,
|
|
Phone: syncUser.Mobile,
|
|
CountryCode: syncUser.Country,
|
|
Address: []string{syncUser.Address},
|
|
Region: util.ReturnAnyNotEmpty(syncUser.Country, syncUser.CountryName),
|
|
Affiliation: affiliation,
|
|
Tag: tag,
|
|
Score: score,
|
|
Ldap: syncUser.Uuid,
|
|
Properties: syncUser.Attributes,
|
|
}
|
|
formatUserPhone(newUser)
|
|
|
|
// Assign user to groups based on memberOf attribute
|
|
userGroups := []string{}
|
|
if ldap.DefaultGroup != "" {
|
|
userGroups = append(userGroups, ldap.DefaultGroup)
|
|
}
|
|
|
|
// Extract group names from memberOf DNs
|
|
for _, memberDn := range syncUser.MemberOf {
|
|
groupName := dnToGroupName(owner, memberDn)
|
|
if groupName != "" {
|
|
userGroups = append(userGroups, groupName)
|
|
}
|
|
}
|
|
|
|
if len(userGroups) > 0 {
|
|
newUser.Groups = userGroups
|
|
}
|
|
|
|
affected, err := AddUser(newUser, "en")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if !affected {
|
|
failedUsers = append(failedUsers, syncUser)
|
|
continue
|
|
}
|
|
|
|
// Trigger webhook for LDAP user sync
|
|
TriggerWebhookForUser("new-user-ldap", newUser)
|
|
}
|
|
}
|
|
|
|
return existUsers, failedUsers, err
|
|
}
|
|
|
|
// SyncLdapGroups syncs LDAP groups/OUs to Casdoor groups with hierarchy
|
|
func SyncLdapGroups(owner string, ldapGroups []LdapGroup, ldapId string) (newGroups int, updatedGroups int, err error) {
|
|
if len(ldapGroups) == 0 {
|
|
return 0, 0, nil
|
|
}
|
|
|
|
// Create a map of DN to group for quick lookup
|
|
dnToGroup := make(map[string]*LdapGroup)
|
|
for i := range ldapGroups {
|
|
dnToGroup[ldapGroups[i].Dn] = &ldapGroups[i]
|
|
}
|
|
|
|
// Get existing groups for this organization
|
|
existingGroups, err := GetGroups(owner)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
existingGroupMap := make(map[string]*Group)
|
|
for _, group := range existingGroups {
|
|
existingGroupMap[group.Name] = group
|
|
}
|
|
|
|
ldap, err := GetLdap(ldapId)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
// Process groups in hierarchical order (parents before children)
|
|
processedGroups := make(map[string]bool)
|
|
var processGroup func(ldapGroup *LdapGroup) error
|
|
|
|
processGroup = func(ldapGroup *LdapGroup) error {
|
|
if processedGroups[ldapGroup.Dn] {
|
|
return nil
|
|
}
|
|
|
|
// Generate group name from DN
|
|
groupName := dnToGroupName(owner, ldapGroup.Dn)
|
|
if groupName == "" {
|
|
return nil
|
|
}
|
|
|
|
// Determine parent
|
|
var parentId string
|
|
var isTopGroup bool
|
|
|
|
if ldapGroup.ParentDn == "" || ldapGroup.ParentDn == ldap.BaseDn {
|
|
isTopGroup = true
|
|
parentId = ""
|
|
} else {
|
|
// Process parent first
|
|
if parentGroup, exists := dnToGroup[ldapGroup.ParentDn]; exists {
|
|
err := processGroup(parentGroup)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parentId = dnToGroupName(owner, ldapGroup.ParentDn)
|
|
} else {
|
|
isTopGroup = true
|
|
}
|
|
}
|
|
|
|
// Check if group already exists
|
|
if existingGroup, exists := existingGroupMap[groupName]; exists {
|
|
// Update existing group
|
|
existingGroup.DisplayName = ldapGroup.Name
|
|
existingGroup.ParentId = parentId
|
|
existingGroup.IsTopGroup = isTopGroup
|
|
existingGroup.Type = "ldap-synced"
|
|
existingGroup.UpdatedTime = util.GetCurrentTime()
|
|
|
|
_, err := UpdateGroup(existingGroup.GetId(), existingGroup)
|
|
if err == nil {
|
|
updatedGroups++
|
|
}
|
|
} else {
|
|
// Create new group
|
|
newGroup := &Group{
|
|
Owner: owner,
|
|
Name: groupName,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
UpdatedTime: util.GetCurrentTime(),
|
|
DisplayName: ldapGroup.Name,
|
|
ParentId: parentId,
|
|
IsTopGroup: isTopGroup,
|
|
Type: "ldap-synced",
|
|
IsEnabled: true,
|
|
}
|
|
|
|
_, err := AddGroup(newGroup)
|
|
if err == nil {
|
|
newGroups++
|
|
existingGroupMap[groupName] = newGroup
|
|
}
|
|
}
|
|
|
|
processedGroups[ldapGroup.Dn] = true
|
|
return nil
|
|
}
|
|
|
|
// Process all groups
|
|
for i := range ldapGroups {
|
|
err := processGroup(&ldapGroups[i])
|
|
if err != nil {
|
|
// Log error but continue processing other groups
|
|
continue
|
|
}
|
|
}
|
|
|
|
return newGroups, updatedGroups, nil
|
|
}
|
|
|
|
// dnToGroupName converts an LDAP DN to a Casdoor group name
|
|
func dnToGroupName(owner, dn string) string {
|
|
if dn == "" {
|
|
return ""
|
|
}
|
|
|
|
// Parse DN to extract meaningful components
|
|
parts := strings.Split(dn, ",")
|
|
|
|
// Build a hierarchical name from DN components (excluding DC parts)
|
|
var nameComponents []string
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
lowerPart := strings.ToLower(part)
|
|
|
|
// Skip DC (domain component) parts
|
|
if strings.HasPrefix(lowerPart, "dc=") {
|
|
continue
|
|
}
|
|
|
|
// Extract value after = sign
|
|
if idx := strings.Index(part, "="); idx != -1 {
|
|
value := part[idx+1:]
|
|
nameComponents = append(nameComponents, value)
|
|
}
|
|
}
|
|
|
|
if len(nameComponents) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Reverse to get top-down hierarchy
|
|
for i, j := 0, len(nameComponents)-1; i < j; i, j = i+1, j-1 {
|
|
nameComponents[i], nameComponents[j] = nameComponents[j], nameComponents[i]
|
|
}
|
|
|
|
// Join with underscore to create a unique group name
|
|
groupName := strings.Join(nameComponents, "_")
|
|
|
|
// Sanitize group name - replace invalid characters with underscores
|
|
// Keep only alphanumeric characters, underscores, and hyphens
|
|
var sanitized strings.Builder
|
|
for _, r := range groupName {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
|
|
sanitized.WriteRune(r)
|
|
} else {
|
|
sanitized.WriteRune('_')
|
|
}
|
|
}
|
|
groupName = sanitized.String()
|
|
|
|
// Remove consecutive underscores and trim
|
|
for strings.Contains(groupName, "__") {
|
|
groupName = strings.ReplaceAll(groupName, "__", "_")
|
|
}
|
|
groupName = strings.Trim(groupName, "_")
|
|
|
|
return groupName
|
|
}
|
|
|
|
func GetExistUuids(owner string, uuids []string) ([]string, error) {
|
|
var existUuids []string
|
|
|
|
// PostgreSQL only supports up to 65535 parameters per query, so we batch the uuids
|
|
const batchSize = 100
|
|
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
|
|
for i := 0; i < len(uuids); i += batchSize {
|
|
end := i + batchSize
|
|
if end > len(uuids) {
|
|
end = len(uuids)
|
|
}
|
|
batch := uuids[i:end]
|
|
|
|
var batchUuids []string
|
|
err := ormer.Engine.Table(tableNamePrefix+"user").Where("owner = ?", owner).Cols("ldap").
|
|
In("ldap", batch).Select("DISTINCT ldap").Find(&batchUuids)
|
|
if err != nil {
|
|
return existUuids, err
|
|
}
|
|
existUuids = append(existUuids, batchUuids...)
|
|
}
|
|
|
|
return existUuids, nil
|
|
}
|
|
|
|
func ResetLdapPassword(user *User, oldPassword string, newPassword string, lang string) error {
|
|
ldaps, err := GetLdaps(user.Owner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, ldapServer := range ldaps {
|
|
conn, err := ldapServer.GetLdapConn()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
|
|
0, 0, false, ldapServer.buildAuthFilterString(user), []string{}, nil)
|
|
|
|
searchResult, err := conn.Conn.Search(searchReq)
|
|
if err != nil {
|
|
conn.Close()
|
|
return err
|
|
}
|
|
|
|
if len(searchResult.Entries) == 0 {
|
|
conn.Close()
|
|
continue
|
|
}
|
|
if len(searchResult.Entries) > 1 {
|
|
conn.Close()
|
|
return errors.New(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
|
|
}
|
|
|
|
userDn := searchResult.Entries[0].DN
|
|
|
|
var pwdEncoded string
|
|
modifyPasswordRequest := goldap.NewModifyRequest(userDn, nil)
|
|
if conn.IsAD {
|
|
utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
|
|
pwdEncoded, err := utf16.NewEncoder().String("\"" + newPassword + "\"")
|
|
if err != nil {
|
|
conn.Close()
|
|
return err
|
|
}
|
|
modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
|
|
modifyPasswordRequest.Replace("userAccountControl", []string{"512"})
|
|
} else if oldPassword != "" {
|
|
modifyPasswordRequestWithOldPassword := goldap.NewPasswordModifyRequest(userDn, oldPassword, newPassword)
|
|
_, err = conn.Conn.PasswordModify(modifyPasswordRequestWithOldPassword)
|
|
if err != nil {
|
|
conn.Close()
|
|
return err
|
|
}
|
|
conn.Close()
|
|
return nil
|
|
} else {
|
|
switch ldapServer.PasswordType {
|
|
case "SSHA":
|
|
pwdEncoded, err = generateSSHA(newPassword)
|
|
break
|
|
case "MD5":
|
|
md5Byte := md5.Sum([]byte(newPassword))
|
|
md5Password := base64.StdEncoding.EncodeToString(md5Byte[:])
|
|
pwdEncoded = "{MD5}" + md5Password
|
|
break
|
|
case "Plain":
|
|
pwdEncoded = newPassword
|
|
break
|
|
default:
|
|
pwdEncoded = newPassword
|
|
break
|
|
}
|
|
modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
|
|
}
|
|
|
|
err = conn.Conn.Modify(modifyPasswordRequest)
|
|
if err != nil {
|
|
conn.Close()
|
|
return err
|
|
}
|
|
conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ldapUser *LdapUser) buildLdapUserName(owner string) (string, error) {
|
|
user := User{}
|
|
uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber)
|
|
has, err := ormer.Engine.Where("owner = ? and (name = ? or name = ?)", owner, ldapUser.Uid, uidWithNumber).Get(&user)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if has {
|
|
if user.Name == ldapUser.Uid {
|
|
return uidWithNumber, nil
|
|
}
|
|
return fmt.Sprintf("%s_%s", uidWithNumber, randstr.Hex(6)), nil
|
|
}
|
|
|
|
if ldapUser.Uid != "" {
|
|
return ldapUser.Uid, nil
|
|
}
|
|
|
|
return ldapUser.Cn, nil
|
|
}
|
|
|
|
func (ldapUser *LdapUser) buildLdapDisplayName() string {
|
|
if ldapUser.DisplayName != "" {
|
|
return ldapUser.DisplayName
|
|
}
|
|
|
|
return ldapUser.Cn
|
|
}
|
|
|
|
func (ldapUser *LdapUser) GetLdapUuid() string {
|
|
if ldapUser.Uuid != "" {
|
|
return ldapUser.Uuid
|
|
}
|
|
if ldapUser.Uid != "" {
|
|
return ldapUser.Uid
|
|
}
|
|
|
|
return ldapUser.Cn
|
|
}
|
|
|
|
func (ldap *Ldap) buildAuthFilterString(user *User) string {
|
|
// Tolerate configs that use "{}" as a placeholder, e.g. "(uid={})".
|
|
// Casdoor doesn't interpolate it; treat it as wildcard so the base filter remains valid.
|
|
baseFilter := strings.TrimSpace(ldap.Filter)
|
|
if baseFilter == "" {
|
|
baseFilter = "(objectClass=*)"
|
|
} else if strings.Contains(baseFilter, "{}") {
|
|
baseFilter = strings.ReplaceAll(baseFilter, "{}", "*")
|
|
}
|
|
|
|
if len(ldap.FilterFields) == 0 {
|
|
return fmt.Sprintf("(&%s(uid=%s))", baseFilter, goldap.EscapeFilter(user.Name))
|
|
}
|
|
|
|
filter := fmt.Sprintf("(&%s(|", baseFilter)
|
|
for _, field := range ldap.FilterFields {
|
|
filter = fmt.Sprintf("%s(%s=%s)", filter, field, goldap.EscapeFilter(user.getFieldFromLdapAttribute(field)))
|
|
}
|
|
filter = fmt.Sprintf("%s))", filter)
|
|
|
|
return filter
|
|
}
|
|
|
|
func (user *User) getFieldFromLdapAttribute(attribute string) string {
|
|
switch attribute {
|
|
case "uid":
|
|
return user.Name
|
|
case "sAMAccountName":
|
|
return user.Name
|
|
case "mail":
|
|
return user.Email
|
|
case "mobile":
|
|
return user.Phone
|
|
case "c", "co":
|
|
return user.Region
|
|
default:
|
|
return ""
|
|
}
|
|
}
|