* [MM-63717] LDAP Wizard skeleton (#31029) * add ldap_wizard component to render its admin components * i18n * test adjustment * keys and props fixes * title fix * fix placeholders * fix value initialization * linting * remove all ...props (except custom component); any->unknown * fix i18n (temp, will be changed in later PR) * better return; simplify function checking/calling * [MM-64259] Sections sidebar and navigation (#31059) * initial sections list sidebar * sidebar highlighting and scroll on click * some tidying up * add custom section titles for section sidebar * i18n * updating border on sections * scss style lint * color -> border-color * simplify activeSectionKey initialization; remove trailing newline * add useSectionNavigation; clean up ldap_wizard and scss; PR comments * extract section of code into renderSidebar() --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> * [MM-64296] Add test connection for connection settings panel (#31190) * button -> ldap test connect api * fix console error by sanitizing value in text component * return detailed error as error; adjust button -> primary, flushLeft * middle of redesigning how we do hover text, first button * add hover text to bools and file uploads * i18n * add LdapSettings as api type; add new endpoint to api yaml * allow testing without first enabling LDAP and saving config * i18n id changes * improve TestLdapConnection to current standards * PR comments * safeDereference; cleaner returns * remove hover markdown; formatting and typing simplification * use button for "More Info"; i18n * finish renaming help_text_hover -> help_text_more_info * fix error output * only send bindpassword if it has been changed * fix: don't send blank bindPassword when it is still ***** * merge conflict * [MM-64480] Refactor Admin Definition (#31280) * move ldap definition to its own file for simplicity & context * refactor admin_definition to eliminate circular dependencies * merge conflicts * before: buggy userHasReadPermissinOnSomeResources; after: fix incorrect snapshot * merge conflict: new bindPasssword definition was left behind; fixed. * merge conflict * [MM-63765] LDAP Wizard: User filter expandable section (#31286) * add "more info" hover to user filter help texts; make wider * add expandable_setting type and component * use Dislosure show/hide pattern for accessibility * fix tooltip scss selectors * fix hover -> more_info; make sure translation files are correct * use join('\n\n') instead of the eslint disable line * Revert "use join('\n\n') instead of the eslint disable line" This reverts commit 274667e875b34703f14fee0706cd28b0125cefc9. * [MM-64482] LDAP Wizard - Test User filters (#31312) * initial cut at UI and backend for test filters * api definitions; mocks * clean up to current standards * [MM-64512] - Test user filters UI (#31355) * result_count -> total_count * json cannot marshal error, returning error as string as god intended * render errors with icon, hover text, and better feedback texts * gather the settings that may be in expandable sections * remove success, use error == "" to indicate success * [MM-64536] LDAP Wizard: Test user attributes (#31373) * LdapFilterTestResult -> LdapDiagnosticResult; FilterName -> TestName * implement test_attributes endpoint and limited frontend (first step) * adding EntriesWithValue * [MM-64550] LDAP Wizard: Test user attributes UI (#31374) * [MM-64551] LDAP Wizard: Test group attributes (#31375) * remove Test LDAP button (not needed); reused helptext for other btn * implement test_group_attributes endpoint; button/client-side paths * [MM-64552] LDAP Wizard: Test group attributes UI (#31376) * implement Test Group Attributes button * simplify helper functions (improves useCallback dependencies) * show the default filter that was used on the backend in the tooltip * show the icon when there's an error (e.g. required filter/attribute) * fix infinite rerendering * fix error after failed save; fix navigation unlocked after save * empty * Adjust message feedback given we don't test the schema anymore * improve css; don't use inline styles * removed unneccesary pointer indirection * improved i18n strings and logic * combining filters/attributes/group attributes endpoints improve types * improve help text for User Filter (it's tricky) * AvailableAttrs -> AvailableAttributes * fix for e2e tests (renamed title) * more e2e fixes * skip broken e2e test --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
311 строки
10 KiB
Go
311 строки
10 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
)
|
|
|
|
// SyncLdap starts an LDAP sync job.
|
|
// If reAddRemovedMembers is true, then members who left or were removed from a team/channel will
|
|
// be re-added; otherwise, they will not be re-added.
|
|
func (a *App) SyncLdap(c request.CTX, reAddRemovedMembers *bool) {
|
|
a.Srv().Go(func() {
|
|
if license := a.Srv().License(); license != nil && *license.Features.LDAP {
|
|
if !*a.Config().LdapSettings.EnableSync {
|
|
c.Logger().Error("LdapSettings.EnableSync is set to false. Skipping LDAP sync.")
|
|
return
|
|
}
|
|
|
|
ldapI := a.Ldap()
|
|
if ldapI == nil {
|
|
c.Logger().Error("Not executing ldap sync because ldap is not available")
|
|
return
|
|
}
|
|
if _, appErr := ldapI.StartSynchronizeJob(c, false, reAddRemovedMembers); appErr != nil {
|
|
c.Logger().Error("Failed to start LDAP sync job")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (a *App) TestLdap(rctx request.CTX) *model.AppError {
|
|
license := a.Srv().License()
|
|
if ldapI := a.LdapDiagnostic(); ldapI != nil && license != nil && *license.Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
|
|
return ldapI.RunTest(rctx)
|
|
}
|
|
|
|
return model.NewAppError("TestLdap",
|
|
"ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
func (a *App) TestLdapConnection(rctx request.CTX, settings model.LdapSettings) *model.AppError {
|
|
license := a.Srv().License()
|
|
ldapI := a.LdapDiagnostic()
|
|
|
|
// NOTE: normally we would test (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync),
|
|
// but we want to allow sysadmins to test the connection without enabling and saving the config first.
|
|
if ldapI != nil && license != nil && model.SafeDereference(license.Features.LDAP) {
|
|
return ldapI.RunTestConnection(rctx, settings)
|
|
}
|
|
|
|
return model.NewAppError("TestLdapConnection",
|
|
"ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
func (a *App) TestLdapDiagnostics(rctx request.CTX, testType model.LdapDiagnosticTestType, settings model.LdapSettings) ([]model.LdapDiagnosticResult, *model.AppError) {
|
|
license := a.Srv().License()
|
|
ldapI := a.LdapDiagnostic()
|
|
|
|
// NOTE: normally we would test (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync),
|
|
// but we want to allow sysadmins to test the connection without enabling and saving the config first.
|
|
if ldapI != nil && license != nil && *license.Features.LDAP {
|
|
return ldapI.RunTestDiagnostics(rctx, testType, settings)
|
|
}
|
|
|
|
return nil, model.NewAppError("TestLdapDiagnostics", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
// GetLdapGroup retrieves a single LDAP group by the given LDAP group id.
|
|
func (a *App) GetLdapGroup(rctx request.CTX, ldapGroupID string) (*model.Group, *model.AppError) {
|
|
var group *model.Group
|
|
|
|
if a.Ldap() != nil {
|
|
var err *model.AppError
|
|
group, err = a.Ldap().GetGroup(rctx, ldapGroupID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
ae := model.NewAppError("GetLdapGroup", "ent.ldap.app_error", map[string]any{"ldap_group_id": ldapGroupID}, "", http.StatusNotImplemented)
|
|
return nil, ae
|
|
}
|
|
|
|
return group, nil
|
|
}
|
|
|
|
// GetAllLdapGroupsPage retrieves all LDAP groups under the configured base DN using the default or configured group
|
|
// filter.
|
|
func (a *App) GetAllLdapGroupsPage(rctx request.CTX, page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError) {
|
|
var groups []*model.Group
|
|
var total int
|
|
|
|
if a.Ldap() != nil {
|
|
var err *model.AppError
|
|
groups, total, err = a.Ldap().GetAllGroupsPage(rctx, page, perPage, opts)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
} else {
|
|
ae := model.NewAppError("GetAllLdapGroupsPage", "ent.ldap.app_error", nil, "", http.StatusNotImplemented)
|
|
return nil, 0, ae
|
|
}
|
|
|
|
return groups, total, nil
|
|
}
|
|
|
|
func (a *App) SwitchEmailToLdap(c request.CTX, email, password, code, ldapLoginId, ldapPassword string) (string, *model.AppError) {
|
|
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
|
|
return "", model.NewAppError("emailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
user, err := a.GetUserByEmail(email)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err := a.CheckPasswordAndAllCriteria(c, user.Id, password, code); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err := a.RevokeAllSessions(c, user.Id); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ldapInterface := a.Ldap()
|
|
if ldapInterface == nil {
|
|
return "", model.NewAppError("SwitchEmailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if err := ldapInterface.SwitchToLdap(c, user.Id, ldapLoginId, ldapPassword); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, a.GetSiteURL()); err != nil {
|
|
c.Logger().Error("Could not send sign in method changed e-mail", mlog.Err(err))
|
|
}
|
|
})
|
|
|
|
return "/login?extra=signin_change", nil
|
|
}
|
|
|
|
func (a *App) SwitchLdapToEmail(c request.CTX, ldapPassword, code, email, newPassword string) (string, *model.AppError) {
|
|
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
|
|
return "", model.NewAppError("ldapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
if !*a.Config().EmailSettings.EnableSignUpWithEmail {
|
|
return "", model.NewAppError("SwitchEmailToLdap", "api.user.auth_switch.not_available.email_signup_disabled.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
if !*a.Config().EmailSettings.EnableSignInWithEmail && !*a.Config().EmailSettings.EnableSignInWithUsername {
|
|
return "", model.NewAppError("SwitchEmailToLdap", "api.user.auth_switch.not_available.login_disabled.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
user, err := a.GetUserByEmail(email)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if user.AuthService != model.UserAuthServiceLdap {
|
|
return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_ldap_account.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
ldapInterface := a.Ldap()
|
|
if ldapInterface == nil || user.AuthData == nil {
|
|
return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
user, err = a.checkLdapUserPasswordAndAllCriteria(c, user, ldapPassword, code)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err := a.UpdatePassword(c, user, newPassword); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err := a.RevokeAllSessions(c, user.Id); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
T := i18n.GetUserTranslations(user.Locale)
|
|
|
|
a.Srv().Go(func() {
|
|
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
|
|
c.Logger().Error("Could not send sign in method changed e-mail", mlog.Err(err))
|
|
}
|
|
})
|
|
|
|
return "/login?extra=signin_change", nil
|
|
}
|
|
|
|
func (a *App) MigrateIdLDAP(c request.CTX, toAttribute string) *model.AppError {
|
|
if ldapI := a.Ldap(); ldapI != nil {
|
|
if err := ldapI.MigrateIDAttribute(c, toAttribute); err != nil {
|
|
switch err := err.(type) {
|
|
case *model.AppError:
|
|
return err
|
|
default:
|
|
return model.NewAppError("IdMigrateLDAP", "ent.ldap_id_migrate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return model.NewAppError("IdMigrateLDAP", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
func (a *App) writeLdapFile(filename string, fileData *multipart.FileHeader) *model.AppError {
|
|
file, err := fileData.Open()
|
|
if err != nil {
|
|
return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.open.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
defer file.Close()
|
|
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
err = a.Srv().platform.SetConfigFile(filename, data)
|
|
if err != nil {
|
|
return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) AddLdapPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
|
|
if err := a.writeLdapFile(model.LdapPublicCertificateName, fileData); err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := a.Config().Clone()
|
|
*cfg.LdapSettings.PublicCertificateFile = model.LdapPublicCertificateName
|
|
|
|
if err := cfg.IsValid(); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) AddLdapPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
|
|
if err := a.writeLdapFile(model.LdapPrivateKeyName, fileData); err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := a.Config().Clone()
|
|
*cfg.LdapSettings.PrivateKeyFile = model.LdapPrivateKeyName
|
|
|
|
if err := cfg.IsValid(); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) removeLdapFile(filename string) *model.AppError {
|
|
if err := a.Srv().platform.RemoveConfigFile(filename); err != nil {
|
|
return model.NewAppError("RemoveLdapFile", "api.admin.remove_certificate.delete.app_error", map[string]any{"Filename": filename}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RemoveLdapPublicCertificate() *model.AppError {
|
|
if err := a.removeLdapFile(*a.Config().LdapSettings.PublicCertificateFile); err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := a.Config().Clone()
|
|
*cfg.LdapSettings.PublicCertificateFile = ""
|
|
|
|
if err := cfg.IsValid(); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RemoveLdapPrivateCertificate() *model.AppError {
|
|
if err := a.removeLdapFile(*a.Config().LdapSettings.PrivateKeyFile); err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := a.Config().Clone()
|
|
*cfg.LdapSettings.PrivateKeyFile = ""
|
|
|
|
if err := cfg.IsValid(); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
|
|
|
|
return nil
|
|
}
|