[MM-63152] LDAP Wizard (#31417)
* [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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
4278296d45
Коммит
548a47ae56
@@ -23,6 +23,9 @@ type mixedUnlinkedGroup struct {
|
||||
func (api *API) InitLdap() {
|
||||
api.BaseRoutes.LDAP.Handle("/sync", api.APISessionRequired(syncLdap)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.LDAP.Handle("/test", api.APISessionRequired(testLdap)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.LDAP.Handle("/test_connection", api.APISessionRequired(testLdapConnection)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.LDAP.Handle("/test_diagnostics", api.APISessionRequired(testLdapDiagnostics)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.LDAP.Handle("/migrateid", api.APISessionRequired(migrateIDLdap)).Methods(http.MethodPost)
|
||||
|
||||
// GET /api/v4/ldap/groups?page=0&per_page=1000
|
||||
@@ -45,7 +48,7 @@ func (api *API) InitLdap() {
|
||||
|
||||
func syncLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
|
||||
c.Err = model.NewAppError("Api4.syncLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
c.Err = model.NewAppError("api4.syncLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -73,7 +76,7 @@ func syncLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func testLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
|
||||
c.Err = model.NewAppError("Api4.testLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
c.Err = model.NewAppError("api4.testLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,6 +93,71 @@ func testLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func testLdapConnection(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil || !model.SafeDereference(c.App.Channels().License().Features.LDAP) {
|
||||
c.Err = model.NewAppError("api4.testLdapConnection", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestLdap) {
|
||||
c.SetPermissionError(model.PermissionTestLdap)
|
||||
return
|
||||
}
|
||||
|
||||
var settings model.LdapSettings
|
||||
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
||||
c.SetInvalidParamWithErr("ldap_settings", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.TestLdapConnection(c.AppContext, settings); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func testLdapDiagnostics(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
|
||||
c.Err = model.NewAppError("Api4.testLdapDiagnostics", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestLdap) {
|
||||
c.SetPermissionError(model.PermissionTestLdap)
|
||||
return
|
||||
}
|
||||
|
||||
testTypeStr := r.URL.Query().Get("test")
|
||||
if testTypeStr == "" {
|
||||
c.SetInvalidParam("test")
|
||||
return
|
||||
}
|
||||
|
||||
testType := model.LdapDiagnosticTestType(testTypeStr)
|
||||
if !testType.IsValid() {
|
||||
c.SetInvalidParam("test")
|
||||
return
|
||||
}
|
||||
|
||||
var settings model.LdapSettings
|
||||
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
||||
c.SetInvalidParamWithErr("ldap_settings", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, appErr := c.App.TestLdapDiagnostics(c.AppContext, testType, settings)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getLdapGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
|
||||
@@ -97,7 +165,7 @@ func getLdapGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
c.Err = model.NewAppError("Api4.getLdapGroups", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
c.Err = model.NewAppError("api4.getLdapGroups", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,7 +203,7 @@ func getLdapGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
Groups []*mixedUnlinkedGroup `json:"groups"`
|
||||
}{Count: total, Groups: mugs})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getLdapGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
c.Err = model.NewAppError("api4.getLdapGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -160,7 +228,7 @@ func linkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
|
||||
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
c.Err = model.NewAppError("api4.linkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -171,7 +239,7 @@ func linkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if ldapGroup == nil {
|
||||
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_group.not_found", nil, "", http.StatusNotFound)
|
||||
c.Err = model.NewAppError("api4.linkLdapGroup", "api.ldap_group.not_found", nil, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -234,7 +302,7 @@ func linkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
b, err := json.Marshal(newOrUpdatedGroup)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
c.Err = model.NewAppError("api4.linkLdapGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -262,7 +330,7 @@ func unlinkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
c.Err = model.NewAppError("Api4.unlinkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
c.Err = model.NewAppError("api4.unlinkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -305,7 +373,7 @@ func migrateIDLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
|
||||
c.Err = model.NewAppError("Api4.idMigrateLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
c.Err = model.NewAppError("api4.idMigrateLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -40,16 +40,38 @@ func (a *App) SyncLdap(c request.CTX, reAddRemovedMembers *bool) {
|
||||
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) {
|
||||
if err := ldapI.RunTest(rctx); err != nil {
|
||||
err.StatusCode = 500
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := model.NewAppError("TestLdap", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return err
|
||||
return ldapI.RunTest(rctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
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.
|
||||
|
||||
@@ -27,4 +27,6 @@ type LdapInterface interface {
|
||||
type LdapDiagnosticInterface interface {
|
||||
RunTest(rctx request.CTX) *model.AppError
|
||||
GetVendorNameAndVendorVersion(rctx request.CTX) (string, string, error)
|
||||
RunTestConnection(rctx request.CTX, settings model.LdapSettings) *model.AppError
|
||||
RunTestDiagnostics(rctx request.CTX, testType model.LdapDiagnosticTestType, settings model.LdapSettings) ([]model.LdapDiagnosticResult, *model.AppError)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,58 @@ func (_m *LdapDiagnosticInterface) RunTest(rctx request.CTX) *model.AppError {
|
||||
return r0
|
||||
}
|
||||
|
||||
// RunTestConnection provides a mock function with given fields: rctx, settings
|
||||
func (_m *LdapDiagnosticInterface) RunTestConnection(rctx request.CTX, settings model.LdapSettings) *model.AppError {
|
||||
ret := _m.Called(rctx, settings)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RunTestConnection")
|
||||
}
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(request.CTX, model.LdapSettings) *model.AppError); ok {
|
||||
r0 = rf(rctx, settings)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RunTestDiagnostics provides a mock function with given fields: rctx, testType, settings
|
||||
func (_m *LdapDiagnosticInterface) RunTestDiagnostics(rctx request.CTX, testType model.LdapDiagnosticTestType, settings model.LdapSettings) ([]model.LdapDiagnosticResult, *model.AppError) {
|
||||
ret := _m.Called(rctx, testType, settings)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RunTestDiagnostics")
|
||||
}
|
||||
|
||||
var r0 []model.LdapDiagnosticResult
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(request.CTX, model.LdapDiagnosticTestType, model.LdapSettings) ([]model.LdapDiagnosticResult, *model.AppError)); ok {
|
||||
return rf(rctx, testType, settings)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(request.CTX, model.LdapDiagnosticTestType, model.LdapSettings) []model.LdapDiagnosticResult); ok {
|
||||
r0 = rf(rctx, testType, settings)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]model.LdapDiagnosticResult)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(request.CTX, model.LdapDiagnosticTestType, model.LdapSettings) *model.AppError); ok {
|
||||
r1 = rf(rctx, testType, settings)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewLdapDiagnosticInterface creates a new instance of LdapDiagnosticInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewLdapDiagnosticInterface(t interface {
|
||||
|
||||
@@ -2276,6 +2276,10 @@
|
||||
"id": "api.job.unable_to_manage_job.incorrect_job_type",
|
||||
"translation": "You do not have permission to manage this job type"
|
||||
},
|
||||
{
|
||||
"id": "api.ldap.invalid_test_type.app_error",
|
||||
"translation": "Invalid test type: {{.TestType}}"
|
||||
},
|
||||
{
|
||||
"id": "api.ldap_group.not_found",
|
||||
"translation": "ldap group not found"
|
||||
@@ -8252,6 +8256,10 @@
|
||||
"id": "ent.ldap.app_error",
|
||||
"translation": "ldap interface was nil."
|
||||
},
|
||||
{
|
||||
"id": "ent.ldap.connection.test_failed",
|
||||
"translation": "LDAP connection test failed. Server: {{.Server}}:{{.Port}}, ConnectionSecurity: {{.ConnectionType}}, PrivateKeyFilename: {{.PrivateKeyFilename}}, PublicCertificateFilename: {{.PublicCertFilename}}, BindUsername: {{.BindUsername}}. Error: {{.Error}}"
|
||||
},
|
||||
{
|
||||
"id": "ent.ldap.cpa_field_mapping.list_error",
|
||||
"translation": "Failed to retrieve CPA fields"
|
||||
|
||||
@@ -8,3 +8,44 @@ const (
|
||||
LdapPublicCertificateName = "ldap-public.crt"
|
||||
LdapPrivateKeyName = "ldap-private.key"
|
||||
)
|
||||
|
||||
// LdapDiagnosticTestType represents the type of LDAP diagnostic test to run
|
||||
type LdapDiagnosticTestType string
|
||||
|
||||
const (
|
||||
LdapDiagnosticTestTypeFilters LdapDiagnosticTestType = "filters"
|
||||
LdapDiagnosticTestTypeAttributes LdapDiagnosticTestType = "attributes"
|
||||
LdapDiagnosticTestTypeGroupAttributes LdapDiagnosticTestType = "group_attributes"
|
||||
)
|
||||
|
||||
// IsValid checks if the LdapDiagnosticTestType is valid
|
||||
func (t LdapDiagnosticTestType) IsValid() bool {
|
||||
switch t {
|
||||
case LdapDiagnosticTestTypeFilters, LdapDiagnosticTestTypeAttributes, LdapDiagnosticTestTypeGroupAttributes:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// For Diagnostic results
|
||||
type LdapDiagnosticResult struct {
|
||||
TestName string `json:"test_name"`
|
||||
TestValue string `json:"test_value"`
|
||||
TotalCount int `json:"total_count"`
|
||||
EntriesWithValue int `json:"entries_with_value"` // For Attributes
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error"`
|
||||
SampleResults []LdapSampleEntry `json:"sample_results"`
|
||||
}
|
||||
|
||||
type LdapSampleEntry struct {
|
||||
DN string `json:"dn"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"` // For groups
|
||||
AvailableAttributes map[string]string `json:"available_attributes,omitempty"`
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user