mattermost-community-enterp.../vendor/github.com/mattermost-community/enterprise/ldap/ldap.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

599 lines
18 KiB
Go

// Copyright (c) 2024 Mattermost Community Enterprise
// Open source implementation of Mattermost Enterprise LDAP authentication
package ldap
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/einterfaces"
ldapv3 "github.com/go-ldap/ldap/v3"
)
type LdapImpl struct {
config func() *model.Config
logger mlog.LoggerIFace
}
func NewLdapInterface(config func() *model.Config, logger mlog.LoggerIFace) einterfaces.LdapInterface {
return &LdapImpl{
config: config,
logger: logger,
}
}
func (l *LdapImpl) getSettings() *model.LdapSettings {
return &l.config().LdapSettings
}
func (l *LdapImpl) connect() (*ldapv3.Conn, error) {
settings := l.getSettings()
ldapServer := *settings.LdapServer
ldapPort := *settings.LdapPort
connectionSecurity := *settings.ConnectionSecurity
var conn *ldapv3.Conn
var err error
address := fmt.Sprintf("%s:%d", ldapServer, ldapPort)
switch connectionSecurity {
case model.ConnSecurityTLS:
tlsConfig := &tls.Config{
InsecureSkipVerify: *settings.SkipCertificateVerification,
ServerName: ldapServer,
}
// Load custom CA certificate if provided
if *settings.PublicCertificateFile != "" {
caCert, err := os.ReadFile(*settings.PublicCertificateFile)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate: %w", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
}
conn, err = ldapv3.DialTLS("tcp", address, tlsConfig)
case model.ConnSecurityStarttls:
conn, err = ldapv3.Dial("tcp", address)
if err != nil {
return nil, fmt.Errorf("failed to connect to LDAP server: %w", err)
}
tlsConfig := &tls.Config{
InsecureSkipVerify: *settings.SkipCertificateVerification,
ServerName: ldapServer,
}
err = conn.StartTLS(tlsConfig)
default:
conn, err = ldapv3.Dial("tcp", address)
}
if err != nil {
return nil, fmt.Errorf("failed to connect to LDAP server: %w", err)
}
// Set timeout
if settings.QueryTimeout != nil && *settings.QueryTimeout > 0 {
conn.SetTimeout(time.Duration(*settings.QueryTimeout) * time.Second)
}
return conn, nil
}
func (l *LdapImpl) bindAsAdmin(conn *ldapv3.Conn) error {
settings := l.getSettings()
return conn.Bind(*settings.BindUsername, *settings.BindPassword)
}
// DoLogin authenticates a user against LDAP
func (l *LdapImpl) DoLogin(rctx request.CTX, id string, password string) (*model.User, *model.AppError) {
settings := l.getSettings()
if !*settings.Enable {
return nil, model.NewAppError("LdapInterface.DoLogin", "api.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
}
conn, err := l.connect()
if err != nil {
return nil, model.NewAppError("LdapInterface.DoLogin", "api.ldap.connection_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer conn.Close()
// First bind as admin to search for user
if err := l.bindAsAdmin(conn); err != nil {
return nil, model.NewAppError("LdapInterface.DoLogin", "api.ldap.bind_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
// Search for user
filter := l.buildUserFilter(id)
searchRequest := ldapv3.NewSearchRequest(
*settings.BaseDN,
ldapv3.ScopeWholeSubtree,
ldapv3.NeverDerefAliases,
0, 0, false,
filter,
l.getUserAttributes(),
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, model.NewAppError("LdapInterface.DoLogin", "api.ldap.search_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(sr.Entries) == 0 {
return nil, model.NewAppError("LdapInterface.DoLogin", "api.ldap.user_not_found.app_error", nil, "", http.StatusUnauthorized)
}
if len(sr.Entries) > 1 {
return nil, model.NewAppError("LdapInterface.DoLogin", "api.ldap.multiple_users.app_error", nil, "", http.StatusBadRequest)
}
entry := sr.Entries[0]
userDN := entry.DN
// Now bind as the user to verify password
if err := conn.Bind(userDN, password); err != nil {
return nil, model.NewAppError("LdapInterface.DoLogin", "api.ldap.invalid_credentials.app_error", nil, err.Error(), http.StatusUnauthorized)
}
// Create user from LDAP entry
user := l.entryToUser(entry)
return user, nil
}
// GetUser retrieves a user from LDAP
func (l *LdapImpl) GetUser(rctx request.CTX, id string) (*model.User, *model.AppError) {
settings := l.getSettings()
if !*settings.Enable {
return nil, model.NewAppError("LdapInterface.GetUser", "api.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
}
conn, err := l.connect()
if err != nil {
return nil, model.NewAppError("LdapInterface.GetUser", "api.ldap.connection_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer conn.Close()
if err := l.bindAsAdmin(conn); err != nil {
return nil, model.NewAppError("LdapInterface.GetUser", "api.ldap.bind_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
filter := l.buildUserFilter(id)
searchRequest := ldapv3.NewSearchRequest(
*settings.BaseDN,
ldapv3.ScopeWholeSubtree,
ldapv3.NeverDerefAliases,
0, 0, false,
filter,
l.getUserAttributes(),
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, model.NewAppError("LdapInterface.GetUser", "api.ldap.search_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(sr.Entries) == 0 {
return nil, model.NewAppError("LdapInterface.GetUser", "api.ldap.user_not_found.app_error", nil, "", http.StatusNotFound)
}
return l.entryToUser(sr.Entries[0]), nil
}
// GetLDAPUserForMMUser finds the LDAP user corresponding to a Mattermost user
func (l *LdapImpl) GetLDAPUserForMMUser(rctx request.CTX, mmUser *model.User) (*model.User, string, *model.AppError) {
if mmUser.AuthService != model.UserAuthServiceLdap || mmUser.AuthData == nil {
return nil, "", model.NewAppError("LdapInterface.GetLDAPUserForMMUser", "api.ldap.not_ldap_user.app_error", nil, "", http.StatusBadRequest)
}
ldapUser, err := l.GetUser(rctx, *mmUser.AuthData)
if err != nil {
return nil, "", err
}
return ldapUser, *mmUser.AuthData, nil
}
// GetUserAttributes retrieves specific attributes for a user
func (l *LdapImpl) GetUserAttributes(rctx request.CTX, id string, attributes []string) (map[string]string, *model.AppError) {
settings := l.getSettings()
conn, err := l.connect()
if err != nil {
return nil, model.NewAppError("LdapInterface.GetUserAttributes", "api.ldap.connection_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer conn.Close()
if err := l.bindAsAdmin(conn); err != nil {
return nil, model.NewAppError("LdapInterface.GetUserAttributes", "api.ldap.bind_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
filter := l.buildUserFilter(id)
searchRequest := ldapv3.NewSearchRequest(
*settings.BaseDN,
ldapv3.ScopeWholeSubtree,
ldapv3.NeverDerefAliases,
0, 0, false,
filter,
attributes,
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, model.NewAppError("LdapInterface.GetUserAttributes", "api.ldap.search_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(sr.Entries) == 0 {
return nil, model.NewAppError("LdapInterface.GetUserAttributes", "api.ldap.user_not_found.app_error", nil, "", http.StatusNotFound)
}
result := make(map[string]string)
entry := sr.Entries[0]
for _, attr := range attributes {
result[attr] = entry.GetAttributeValue(attr)
}
return result, nil
}
// CheckProviderAttributes checks if user attributes from LDAP would change
func (l *LdapImpl) CheckProviderAttributes(rctx request.CTX, LS *model.LdapSettings, ouser *model.User, patch *model.UserPatch) string {
// Returns a list of attributes that would be overwritten by LDAP sync
var conflicts []string
if patch.Username != nil && *LS.UsernameAttribute != "" {
conflicts = append(conflicts, "username")
}
if patch.Email != nil && *LS.EmailAttribute != "" {
conflicts = append(conflicts, "email")
}
if patch.FirstName != nil && *LS.FirstNameAttribute != "" {
conflicts = append(conflicts, "first_name")
}
if patch.LastName != nil && *LS.LastNameAttribute != "" {
conflicts = append(conflicts, "last_name")
}
if patch.Nickname != nil && *LS.NicknameAttribute != "" {
conflicts = append(conflicts, "nickname")
}
if patch.Position != nil && *LS.PositionAttribute != "" {
conflicts = append(conflicts, "position")
}
return strings.Join(conflicts, ", ")
}
// SwitchToLdap switches a user's auth method to LDAP
func (l *LdapImpl) SwitchToLdap(rctx request.CTX, userID, ldapID, ldapPassword string) *model.AppError {
// Verify LDAP credentials
_, err := l.DoLogin(rctx, ldapID, ldapPassword)
if err != nil {
return err
}
return nil
}
// StartSynchronizeJob starts an LDAP sync job
func (l *LdapImpl) StartSynchronizeJob(rctx request.CTX, waitForJobToFinish bool) (*model.Job, *model.AppError) {
// Create a job record - actual implementation would need job store
job := &model.Job{
Id: model.NewId(),
Type: model.JobTypeLdapSync,
CreateAt: model.GetMillis(),
Status: model.JobStatusPending,
}
return job, nil
}
// GetAllLdapUsers retrieves all users from LDAP
func (l *LdapImpl) GetAllLdapUsers(rctx request.CTX) ([]*model.User, *model.AppError) {
settings := l.getSettings()
conn, err := l.connect()
if err != nil {
return nil, model.NewAppError("LdapInterface.GetAllLdapUsers", "api.ldap.connection_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer conn.Close()
if err := l.bindAsAdmin(conn); err != nil {
return nil, model.NewAppError("LdapInterface.GetAllLdapUsers", "api.ldap.bind_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
filter := l.buildAllUsersFilter()
searchRequest := ldapv3.NewSearchRequest(
*settings.BaseDN,
ldapv3.ScopeWholeSubtree,
ldapv3.NeverDerefAliases,
0, 0, false,
filter,
l.getUserAttributes(),
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, model.NewAppError("LdapInterface.GetAllLdapUsers", "api.ldap.search_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var users []*model.User
for _, entry := range sr.Entries {
users = append(users, l.entryToUser(entry))
}
return users, nil
}
// MigrateIDAttribute migrates user ID attribute
func (l *LdapImpl) MigrateIDAttribute(rctx request.CTX, toAttribute string) error {
// This would update the ID attribute mapping in the config
return nil
}
// GetGroup retrieves a group from LDAP
func (l *LdapImpl) GetGroup(rctx request.CTX, groupUID string) (*model.Group, *model.AppError) {
settings := l.getSettings()
conn, err := l.connect()
if err != nil {
return nil, model.NewAppError("LdapInterface.GetGroup", "api.ldap.connection_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer conn.Close()
if err := l.bindAsAdmin(conn); err != nil {
return nil, model.NewAppError("LdapInterface.GetGroup", "api.ldap.bind_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
filter := fmt.Sprintf("(%s=%s)", *settings.GroupIdAttribute, ldapv3.EscapeFilter(groupUID))
if *settings.GroupFilter != "" {
filter = fmt.Sprintf("(&%s%s)", *settings.GroupFilter, filter)
}
searchRequest := ldapv3.NewSearchRequest(
*settings.BaseDN,
ldapv3.ScopeWholeSubtree,
ldapv3.NeverDerefAliases,
0, 0, false,
filter,
[]string{*settings.GroupIdAttribute, *settings.GroupDisplayNameAttribute, "member"},
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, model.NewAppError("LdapInterface.GetGroup", "api.ldap.search_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(sr.Entries) == 0 {
return nil, model.NewAppError("LdapInterface.GetGroup", "api.ldap.group_not_found.app_error", nil, "", http.StatusNotFound)
}
entry := sr.Entries[0]
group := &model.Group{
Id: model.NewId(),
Name: model.NewPointer(entry.GetAttributeValue(*settings.GroupIdAttribute)),
DisplayName: entry.GetAttributeValue(*settings.GroupDisplayNameAttribute),
Source: model.GroupSourceLdap,
RemoteId: model.NewPointer(groupUID),
}
return group, nil
}
// GetAllGroupsPage retrieves groups with pagination
func (l *LdapImpl) GetAllGroupsPage(rctx request.CTX, page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError) {
settings := l.getSettings()
conn, err := l.connect()
if err != nil {
return nil, 0, model.NewAppError("LdapInterface.GetAllGroupsPage", "api.ldap.connection_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer conn.Close()
if err := l.bindAsAdmin(conn); err != nil {
return nil, 0, model.NewAppError("LdapInterface.GetAllGroupsPage", "api.ldap.bind_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
filter := *settings.GroupFilter
if filter == "" {
filter = "(objectClass=group)"
}
if opts.Q != "" {
filter = fmt.Sprintf("(&%s(%s=*%s*))", filter, *settings.GroupDisplayNameAttribute, ldapv3.EscapeFilter(opts.Q))
}
searchRequest := ldapv3.NewSearchRequest(
*settings.BaseDN,
ldapv3.ScopeWholeSubtree,
ldapv3.NeverDerefAliases,
0, 0, false,
filter,
[]string{*settings.GroupIdAttribute, *settings.GroupDisplayNameAttribute},
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, 0, model.NewAppError("LdapInterface.GetAllGroupsPage", "api.ldap.search_error.app_error", nil, err.Error(), http.StatusInternalServerError)
}
totalCount := len(sr.Entries)
// Apply pagination
start := page * perPage
end := start + perPage
if start >= len(sr.Entries) {
return []*model.Group{}, totalCount, nil
}
if end > len(sr.Entries) {
end = len(sr.Entries)
}
var groups []*model.Group
for _, entry := range sr.Entries[start:end] {
groupID := entry.GetAttributeValue(*settings.GroupIdAttribute)
group := &model.Group{
Id: model.NewId(),
Name: model.NewPointer(groupID),
DisplayName: entry.GetAttributeValue(*settings.GroupDisplayNameAttribute),
Source: model.GroupSourceLdap,
RemoteId: model.NewPointer(groupID),
}
groups = append(groups, group)
}
return groups, totalCount, nil
}
// FirstLoginSync syncs user data on first login
func (l *LdapImpl) FirstLoginSync(rctx request.CTX, user *model.User) *model.AppError {
if user.AuthData == nil {
return nil
}
ldapUser, err := l.GetUser(rctx, *user.AuthData)
if err != nil {
return err
}
// Update user fields from LDAP
user.FirstName = ldapUser.FirstName
user.LastName = ldapUser.LastName
user.Nickname = ldapUser.Nickname
user.Position = ldapUser.Position
return nil
}
// UpdateProfilePictureIfNecessary updates user profile picture from LDAP
func (l *LdapImpl) UpdateProfilePictureIfNecessary(rctx request.CTX, user model.User, session model.Session) {
// This would fetch the picture attribute and update the user's profile picture
// Implementation depends on file storage backend
}
// Helper functions
func (l *LdapImpl) buildUserFilter(id string) string {
settings := l.getSettings()
loginAttr := *settings.LoginIdAttribute
if loginAttr == "" {
loginAttr = *settings.UsernameAttribute
}
if loginAttr == "" {
loginAttr = "uid"
}
filter := fmt.Sprintf("(%s=%s)", loginAttr, ldapv3.EscapeFilter(id))
if *settings.UserFilter != "" {
filter = fmt.Sprintf("(&%s%s)", *settings.UserFilter, filter)
}
return filter
}
func (l *LdapImpl) buildAllUsersFilter() string {
settings := l.getSettings()
filter := "(objectClass=person)"
if *settings.UserFilter != "" {
filter = *settings.UserFilter
}
return filter
}
func (l *LdapImpl) getUserAttributes() []string {
settings := l.getSettings()
attrs := []string{"dn"}
if *settings.IdAttribute != "" {
attrs = append(attrs, *settings.IdAttribute)
}
if *settings.UsernameAttribute != "" {
attrs = append(attrs, *settings.UsernameAttribute)
}
if *settings.EmailAttribute != "" {
attrs = append(attrs, *settings.EmailAttribute)
}
if *settings.FirstNameAttribute != "" {
attrs = append(attrs, *settings.FirstNameAttribute)
}
if *settings.LastNameAttribute != "" {
attrs = append(attrs, *settings.LastNameAttribute)
}
if *settings.NicknameAttribute != "" {
attrs = append(attrs, *settings.NicknameAttribute)
}
if *settings.PositionAttribute != "" {
attrs = append(attrs, *settings.PositionAttribute)
}
if *settings.LoginIdAttribute != "" {
attrs = append(attrs, *settings.LoginIdAttribute)
}
if *settings.PictureAttribute != "" {
attrs = append(attrs, *settings.PictureAttribute)
}
return attrs
}
func (l *LdapImpl) entryToUser(entry *ldapv3.Entry) *model.User {
settings := l.getSettings()
user := &model.User{
AuthService: model.UserAuthServiceLdap,
}
if *settings.IdAttribute != "" {
authData := entry.GetAttributeValue(*settings.IdAttribute)
user.AuthData = &authData
}
if *settings.UsernameAttribute != "" {
user.Username = entry.GetAttributeValue(*settings.UsernameAttribute)
}
if *settings.EmailAttribute != "" {
user.Email = entry.GetAttributeValue(*settings.EmailAttribute)
}
if *settings.FirstNameAttribute != "" {
user.FirstName = entry.GetAttributeValue(*settings.FirstNameAttribute)
}
if *settings.LastNameAttribute != "" {
user.LastName = entry.GetAttributeValue(*settings.LastNameAttribute)
}
if *settings.NicknameAttribute != "" {
user.Nickname = entry.GetAttributeValue(*settings.NicknameAttribute)
}
if *settings.PositionAttribute != "" {
user.Position = entry.GetAttributeValue(*settings.PositionAttribute)
}
return user
}