From eba38625eb780f4abbe90df2ae71302382a4966d Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Mon, 14 Sep 2020 12:53:42 -0600 Subject: [PATCH] Implement LDAP Certificate (#15361) * Implement LDAP Certificate * add diagnostics and translations * update from code review * pass pointer to update pict function * pass object to first function * remove debug log messages * update test to add localmode test * update lint errors Co-authored-by: Mattermod --- api4/ldap.go | 112 +++++++++++++++++++++++ api4/ldap_local.go | 5 ++ api4/ldap_test.go | 128 +++++++++++++++++++++++++++ app/app_iface.go | 4 + app/ldap.go | 97 ++++++++++++++++++++ app/login.go | 2 +- app/opentracing/opentracing_layer.go | 88 ++++++++++++++++++ einterfaces/ldap.go | 2 +- einterfaces/mocks/LdapInterface.go | 2 +- i18n/en.json | 16 ++++ model/client4.go | 50 ++++++++++- model/config.go | 16 +++- model/ldap.go | 4 +- services/telemetry/telemetry.go | 2 + 14 files changed, 517 insertions(+), 11 deletions(-) diff --git a/api4/ldap.go b/api4/ldap.go index 41d649e975..ea66d4fd31 100644 --- a/api4/ldap.go +++ b/api4/ldap.go @@ -6,6 +6,7 @@ package api4 import ( "database/sql" "encoding/json" + "mime/multipart" "net/http" "github.com/mattermost/mattermost-server/v5/audit" @@ -32,6 +33,13 @@ func (api *API) InitLdap() { // DELETE /api/v4/ldap/groups/:remote_id/link api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.ApiSessionRequired(unlinkLdapGroup)).Methods("DELETE") + + api.BaseRoutes.LDAP.Handle("/certificate/public", api.ApiSessionRequired(addLdapPublicCertificate)).Methods("POST") + api.BaseRoutes.LDAP.Handle("/certificate/private", api.ApiSessionRequired(addLdapPrivateCertificate)).Methods("POST") + + api.BaseRoutes.LDAP.Handle("/certificate/public", api.ApiSessionRequired(removeLdapPublicCertificate)).Methods("DELETE") + api.BaseRoutes.LDAP.Handle("/certificate/private", api.ApiSessionRequired(removeLdapPrivateCertificate)).Methods("DELETE") + } func syncLdap(c *Context, w http.ResponseWriter, r *http.Request) { @@ -290,3 +298,107 @@ func migrateIdLdap(c *Context, w http.ResponseWriter, r *http.Request) { auditRec.Success() ReturnStatusOK(w) } + +func parseLdapCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) { + err := r.ParseMultipartForm(maxFileSize) + if err != nil { + return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.parseform.app_error", nil, err.Error(), http.StatusBadRequest) + } + + m := r.MultipartForm + + fileArray, ok := m.File["certificate"] + if !ok { + return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest) + } + + if len(fileArray) <= 0 { + return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.array.app_error", nil, "", http.StatusBadRequest) + } + + return fileArray[0], nil +} + +func addLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize) + if err != nil { + c.Err = err + return + } + + auditRec := c.MakeAuditRecord("addLdapPublicCertificate", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("filename", fileData.Filename) + + if err := c.App.AddLdapPublicCertificate(fileData); err != nil { + c.Err = err + return + } + auditRec.Success() + ReturnStatusOK(w) +} + +func addLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize) + if err != nil { + c.Err = err + return + } + + auditRec := c.MakeAuditRecord("addLdapPrivateCertificate", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("filename", fileData.Filename) + + if err := c.App.AddLdapPrivateCertificate(fileData); err != nil { + c.Err = err + return + } + auditRec.Success() + ReturnStatusOK(w) +} + +func removeLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + auditRec := c.MakeAuditRecord("removeLdapPublicCertificate", audit.Fail) + defer c.LogAuditRec(auditRec) + + if err := c.App.RemoveLdapPublicCertificate(); err != nil { + c.Err = err + return + } + + auditRec.Success() + ReturnStatusOK(w) +} + +func removeLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + auditRec := c.MakeAuditRecord("removeLdapPrivateCertificate", audit.Fail) + defer c.LogAuditRec(auditRec) + + if err := c.App.RemoveLdapPrivateCertificate(); err != nil { + c.Err = err + return + } + + auditRec.Success() + ReturnStatusOK(w) +} diff --git a/api4/ldap_local.go b/api4/ldap_local.go index 101585db3c..f82cf52edb 100644 --- a/api4/ldap_local.go +++ b/api4/ldap_local.go @@ -8,4 +8,9 @@ func (api *API) InitLdapLocal() { api.BaseRoutes.LDAP.Handle("/sync", api.ApiLocal(syncLdap)).Methods("POST") api.BaseRoutes.LDAP.Handle("/test", api.ApiLocal(testLdap)).Methods("POST") api.BaseRoutes.LDAP.Handle("/groups", api.ApiLocal(getLdapGroups)).Methods("GET") + api.BaseRoutes.LDAP.Handle("/certificate/public", api.ApiLocal(addLdapPublicCertificate)).Methods("POST") + api.BaseRoutes.LDAP.Handle("/certificate/private", api.ApiLocal(addLdapPrivateCertificate)).Methods("POST") + api.BaseRoutes.LDAP.Handle("/certificate/public", api.ApiLocal(removeLdapPublicCertificate)).Methods("DELETE") + api.BaseRoutes.LDAP.Handle("/certificate/private", api.ApiLocal(removeLdapPrivateCertificate)).Methods("DELETE") + } diff --git a/api4/ldap_test.go b/api4/ldap_test.go index 298410eb56..ef092bab40 100644 --- a/api4/ldap_test.go +++ b/api4/ldap_test.go @@ -11,6 +11,92 @@ import ( "github.com/mattermost/mattermost-server/v5/model" ) +var spPrivateKey = `-----BEGIN PRIVATE KEY----- +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 +kSU80FrXITorOgZogkDk0FVpJA3qvQ== +-----END PRIVATE KEY-----` + +var spPublicCertificate = `-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIJAIRQ3EwrvOprMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV +BAYTAlVTMRIwEAYDVQQHDAlQYWxvIEFsdG8xEzARBgNVBAoMCk1hdHRlcm1vc3Qx +DzANBgNVBAsMBkRldk9wczETMBEGA1UEAwwKY2xpZW50LmNvbTAeFw0xOTA5MTIx +NzM1MzdaFw0yOTA5MDkxNzM1MzdaMFwxCzAJBgNVBAYTAlVTMRIwEAYDVQQHDAlQ +YWxvIEFsdG8xEzARBgNVBAoMCk1hdHRlcm1vc3QxDzANBgNVBAsMBkRldk9wczET +MBEGA1UEAwwKY2xpZW50LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBANtVtR87yAUOCrHDdnuBfmVrJMopdBPrbeHR5Qdej3mMh0CkzIJ2FFihjP05 +S+jTVpDRTIStzJYpQkTcTRgQ/8yO8EZMh6aaSP81Ba0uojFlkWeXy5VybmA9ZMmA +SHPL2Lj8Pj2qiVhqQk5WQr5+cBZTMiI7kYgzoi3zO4Mgr1ryWcsroIZaFZQNHyzq +eXE6aIbfYtEo8A4muXE09sMk7lJX9aS17/VR1UJH1xbkWugnlp79xQSkx42mIUwJ +zj2AM8Q4s5MCuhizVs6FLiYYCuU7RZlTPftzJlKj8y1YmjZjtuRV6MC3ql0xya9g +3LnJ12uyUD7Q6K15lLh03TW0drhCIRo1Vn4N0pZ+kkViqpU02r9gy95SOTshuecm +nP7KU5zzM2d6iLATkIj6aYWnRfT5m3UgkPsFdkTDfsmGT5h4F1BSeQc9YoePh2Qo ++l+M9f8rWvYbUJjuIMbdXV8+wgUB8JS8ttYf33iFp7PP1T6Fe7z0uaGcqrrfx8N2 +4ZgpA7JjPz8GxDUxA/mQKsODkeGepTH90QISUWUcKerdn+AehA6JzEDx/UZ4IOIg +TZUVhlFAsMvbWVv2jNAEVstll8ZosaZQ+F5xDRGtD9bhg82EG/JcQs6MKwN+7qW2 +ehvoTwu7Huw2iEW4tpiDQDMNF97OFTRGpZdREqWmKobSpqhbAgMBAAGjTzBNMBIG +A1UdEwEB/wQIMAYBAf8CAQAwNwYDVR0RBDAwLoIOd3d3LmNsaWVudC5jb22CEGFk +bWluLmNsaWVudC5jb22HBMCoAQqHBAoAAOowDQYJKoZIhvcNAQELBQADggIBAFEI +D1ySRS+lQYVm24PPIUH5OmBEJUsVKI/zUXEQ4hdqEqN4UA3NGKkujajTz2fStaOj +LfGDup1ZQRYG6VVvNwbZHX9G9mb8TyZ12XFLVjPTbxoG+NZb3ipue9S6qZcT9WEF +sjaXhkVNhhVc1GOMnv/FNiclLPWLMnR8WST+Y+WSsT59wP40kJynaT7wQt2TmImg +kQfM69jQNgAkyrFwO8y1YcnH7Avrw9YvzhUWG2FfNCTTVNb+StxNtqGwvDV33iZ2 +bBUWIy2fsNUA4tUYK31Ye6thJiKmvy/LqVJ415gPsI3zHzTCLU/GBUCNCNnEDnhU +KO2K3mk1wK3sshMGcda/Xz2a9TfkIxs0pkenS57bZ8xT7mxBzXsZGm7Mnb2fujmX +fBEyxQ2ot0Nl9Lp26WrBjQZojJ10Ic2IRxU3spC/FYK7BenQEAdnNHkyQ3lowAto +NpOL+j+1ooksPQbp4DeIBbrZDNKvFot+ja2aDJ738sgXf8ht7kGXA5DPNtPLsmUr +wpZrhxKD6pXVPhA6EeG2efdUP1ODslmehl4t2yX+FqHChnl7E012W8Cf0Ugybp1t +15IXg8GxCRENSNAwpOvTMkoonHqNvBkaCDZHtxeyJMJWQW1B0Xek1JY3CNHvnY7I +MCOV5SHi05kD42JSSbmw190VAa4QRGikaeWRhDsj +-----END CERTIFICATE-----` + func TestTestLdap(t *testing.T) { th := Setup(t) defer th.TearDown() @@ -112,3 +198,45 @@ func TestMigrateIdLdap(t *testing.T) { CheckNotImplementedStatus(t, resp) }) } + +func TestUploadPublicCertificate(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + _, resp := th.Client.UploadLdapPublicCertificate([]byte(spPublicCertificate)) + require.NotNil(t, resp.Error, "Should have failed. No System Admin privileges") + + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + _, resp = client.UploadLdapPublicCertificate([]byte(spPrivateKey)) + require.Nil(t, resp.Error, "Should have passed. System Admin privileges %v", resp.Error) + }) + + _, resp = th.Client.DeleteLdapPublicCertificate() + require.NotNil(t, resp.Error, "Should have failed. No System Admin privileges") + + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + _, resp := client.DeleteLdapPublicCertificate() + require.Nil(t, resp.Error, "Should have passed. System Admin privileges %v", resp.Error) + }) +} + +func TestUploadPrivateCertificate(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + _, resp := th.Client.UploadLdapPrivateCertificate([]byte(spPrivateKey)) + require.NotNil(t, resp.Error, "Should have failed. No System Admin privileges") + + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + _, resp = client.UploadLdapPrivateCertificate([]byte(spPrivateKey)) + require.Nil(t, resp.Error, "Should have passed. System Admin privileges %v", resp.Error) + }) + + _, resp = th.Client.DeleteLdapPrivateCertificate() + require.NotNil(t, resp.Error, "Should have failed. No System Admin privileges") + + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + _, resp := client.DeleteLdapPrivateCertificate() + require.Nil(t, resp.Error, "Should have passed. System Admin privileges %v", resp.Error) + }) +} diff --git a/app/app_iface.go b/app/app_iface.go index deafbc50e4..25e789c21f 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -341,6 +341,8 @@ type AppIface interface { AddChannelMember(userId string, channel *model.Channel, userRequestorId string, postRootId string) (*model.ChannelMember, *model.AppError) AddConfigListener(listener func(*model.Config, *model.Config)) string AddDirectChannels(teamId string, user *model.User) *model.AppError + AddLdapPrivateCertificate(fileData *multipart.FileHeader) *model.AppError + AddLdapPublicCertificate(fileData *multipart.FileHeader) *model.AppError AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError @@ -807,6 +809,8 @@ type AppIface interface { RemoveAllDeactivatedMembersFromChannel(channel *model.Channel) *model.AppError RemoveConfigListener(id string) RemoveFile(path string) *model.AppError + RemoveLdapPrivateCertificate() *model.AppError + RemoveLdapPublicCertificate() *model.AppError RemovePlugin(id string) *model.AppError RemovePluginFromData(data model.PluginEventData) RemoveSamlIdpCertificate() *model.AppError diff --git a/app/ldap.go b/app/ldap.go index cef00e0807..e717701367 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -4,6 +4,8 @@ package app import ( + "io/ioutil" + "mime/multipart" "net/http" "github.com/mattermost/mattermost-server/v5/mlog" @@ -175,3 +177,98 @@ func (a *App) MigrateIdLDAP(toAttribute string) *model.AppError { } 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, err.Error(), http.StatusInternalServerError) + } + defer file.Close() + + data, err := ioutil.ReadAll(file) + if err != nil { + return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + err = a.Srv().configStore.SetFile(filename, data) + if err != nil { + return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return nil +} + +func (a *App) AddLdapPublicCertificate(fileData *multipart.FileHeader) *model.AppError { + if err := a.writeLdapFile(model.LDAP_PUBIC_CERTIFICATE_NAME, fileData); err != nil { + return err + } + + cfg := a.Config().Clone() + *cfg.LdapSettings.PublicCertificateFile = model.LDAP_PUBIC_CERTIFICATE_NAME + + 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.LDAP_PRIVATE_KEY_NAME, fileData); err != nil { + return err + } + + cfg := a.Config().Clone() + *cfg.LdapSettings.PrivateKeyFile = model.LDAP_PRIVATE_KEY_NAME + + 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().configStore.RemoveFile(filename); err != nil { + return model.NewAppError("RemoveLdapFile", "api.admin.remove_certificate.delete.app_error", map[string]interface{}{"Filename": filename}, err.Error(), http.StatusInternalServerError) + } + 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 +} diff --git a/app/login.go b/app/login.go index 33e20f8c75..f992e32dc3 100644 --- a/app/login.go +++ b/app/login.go @@ -218,7 +218,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, if a.Srv().License() != nil && *a.Srv().License().Features.LDAP && a.Ldap() != nil { a.Srv().Go(func() { - a.Ldap().UpdateProfilePictureIfNecessary(user, session) + a.Ldap().UpdateProfilePictureIfNecessary(*user, session) }) } diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 9f2336cba6..f387ea41fc 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -170,6 +170,50 @@ func (a *OpenTracingAppLayer) AddDirectChannels(teamId string, user *model.User) return resultVar0 } +func (a *OpenTracingAppLayer) AddLdapPrivateCertificate(fileData *multipart.FileHeader) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddLdapPrivateCertificate") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.AddLdapPrivateCertificate(fileData) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + +func (a *OpenTracingAppLayer) AddLdapPublicCertificate(fileData *multipart.FileHeader) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddLdapPublicCertificate") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.AddLdapPublicCertificate(fileData) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) AddPublicKey(name string, key io.Reader) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddPublicKey") @@ -11340,6 +11384,50 @@ func (a *OpenTracingAppLayer) RemoveFile(path string) *model.AppError { return resultVar0 } +func (a *OpenTracingAppLayer) RemoveLdapPrivateCertificate() *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveLdapPrivateCertificate") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.RemoveLdapPrivateCertificate() + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + +func (a *OpenTracingAppLayer) RemoveLdapPublicCertificate() *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveLdapPublicCertificate") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.RemoveLdapPublicCertificate() + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) RemovePlugin(id string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemovePlugin") diff --git a/einterfaces/ldap.go b/einterfaces/ldap.go index 6a4f9bd1d9..bcc7b854e0 100644 --- a/einterfaces/ldap.go +++ b/einterfaces/ldap.go @@ -21,6 +21,6 @@ type LdapInterface interface { GetGroup(groupUID string) (*model.Group, *model.AppError) GetAllGroupsPage(page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError) FirstLoginSync(userID, userAuthService, userAuthData, email string) *model.AppError - UpdateProfilePictureIfNecessary(*model.User, *model.Session) + UpdateProfilePictureIfNecessary(model.User, *model.Session) GetADLdapIdFromSAMLId(authData string) string } diff --git a/einterfaces/mocks/LdapInterface.go b/einterfaces/mocks/LdapInterface.go index ca6bade8c5..6c68d3431a 100644 --- a/einterfaces/mocks/LdapInterface.go +++ b/einterfaces/mocks/LdapInterface.go @@ -305,6 +305,6 @@ func (_m *LdapInterface) SwitchToLdap(userId string, ldapId string, ldapPassword } // UpdateProfilePictureIfNecessary provides a mock function with given fields: _a0, _a1 -func (_m *LdapInterface) UpdateProfilePictureIfNecessary(_a0 *model.User, _a1 *model.Session) { +func (_m *LdapInterface) UpdateProfilePictureIfNecessary(_a0 model.User, _a1 *model.Session) { _m.Called(_a0, _a1) } diff --git a/i18n/en.json b/i18n/en.json index cdf211d921..70b7399449 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -59,6 +59,10 @@ "id": "api.admin.add_certificate.open.app_error", "translation": "Could not open certificate file." }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Error parsing multiform request" + }, { "id": "api.admin.add_certificate.saving.app_error", "translation": "Could not save certificate file." @@ -5490,10 +5494,18 @@ "id": "ent.ldap.do_login.bind_admin_user.app_error", "translation": "Unable to bind to AD/LDAP server. Check BindUsername and BindPassword." }, + { + "id": "ent.ldap.do_login.certificate.app_error", + "translation": "Error loading LDAP TLS Certificate file." + }, { "id": "ent.ldap.do_login.invalid_password.app_error", "translation": "Invalid Password." }, + { + "id": "ent.ldap.do_login.key.app_error", + "translation": "Error loading LDAP TLS Key file." + }, { "id": "ent.ldap.do_login.licence_disable.app_error", "translation": "AD/LDAP functionality disabled by current license. Please contact your system administrator about upgrading your enterprise license." @@ -5518,6 +5530,10 @@ "id": "ent.ldap.do_login.user_not_registered.app_error", "translation": "User not registered on AD/LDAP server." }, + { + "id": "ent.ldap.do_login.x509.app_error", + "translation": "Error creating key pair" + }, { "id": "ent.ldap.save_user.email_exists.ldap_app_error", "translation": "This account does not use AD/LDAP authentication. Please sign in using email and password." diff --git a/model/client4.go b/model/client4.go index 6ab34f1ac3..dbfd8e88e7 100644 --- a/model/client4.go +++ b/model/client4.go @@ -3671,7 +3671,7 @@ func (c *Client4) GetSamlMetadata() (string, *Response) { return buf.String(), BuildResponse(r) } -func samlFileToMultipart(data []byte, filename string) ([]byte, *multipart.Writer, error) { +func fileToMultipart(data []byte, filename string) ([]byte, *multipart.Writer, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -3694,7 +3694,7 @@ func samlFileToMultipart(data []byte, filename string) ([]byte, *multipart.Write // UploadSamlIdpCertificate will upload an IDP certificate for SAML and set the config to use it. // The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk. func (c *Client4) UploadSamlIdpCertificate(data []byte, filename string) (bool, *Response) { - body, writer, err := samlFileToMultipart(data, filename) + body, writer, err := fileToMultipart(data, filename) if err != nil { return false, &Response{Error: NewAppError("UploadSamlIdpCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest)} } @@ -3706,7 +3706,7 @@ func (c *Client4) UploadSamlIdpCertificate(data []byte, filename string) (bool, // UploadSamlPublicCertificate will upload a public certificate for SAML and set the config to use it. // The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk. func (c *Client4) UploadSamlPublicCertificate(data []byte, filename string) (bool, *Response) { - body, writer, err := samlFileToMultipart(data, filename) + body, writer, err := fileToMultipart(data, filename) if err != nil { return false, &Response{Error: NewAppError("UploadSamlPublicCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest)} } @@ -3718,7 +3718,7 @@ func (c *Client4) UploadSamlPublicCertificate(data []byte, filename string) (boo // UploadSamlPrivateCertificate will upload a private key for SAML and set the config to use it. // The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk. func (c *Client4) UploadSamlPrivateCertificate(data []byte, filename string) (bool, *Response) { - body, writer, err := samlFileToMultipart(data, filename) + body, writer, err := fileToMultipart(data, filename) if err != nil { return false, &Response{Error: NewAppError("UploadSamlPrivateCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest)} } @@ -4079,6 +4079,48 @@ func (c *Client4) MigrateAuthToSaml(fromAuthService string, usersMap map[string] return CheckStatusOK(r), BuildResponse(r) } +// UploadLdapPublicCertificate will upload a public certificate for LDAP and set the config to use it. +func (c *Client4) UploadLdapPublicCertificate(data []byte) (bool, *Response) { + body, writer, err := fileToMultipart(data, LDAP_PUBIC_CERTIFICATE_NAME) + if err != nil { + return false, &Response{Error: NewAppError("UploadLdapPublicCertificate", "model.client.upload_ldap_cert.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + _, resp := c.DoUploadFile(c.GetLdapRoute()+"/certificate/public", body, writer.FormDataContentType()) + return resp.Error == nil, resp +} + +// UploadLdapPrivateCertificate will upload a private key for LDAP and set the config to use it. +func (c *Client4) UploadLdapPrivateCertificate(data []byte) (bool, *Response) { + body, writer, err := fileToMultipart(data, LDAP_PRIVATE_KEY_NAME) + if err != nil { + return false, &Response{Error: NewAppError("UploadLdapPrivateCertificate", "model.client.upload_Ldap_cert.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + _, resp := c.DoUploadFile(c.GetLdapRoute()+"/certificate/private", body, writer.FormDataContentType()) + return resp.Error == nil, resp +} + +// DeleteLdapPublicCertificate deletes the LDAP IDP certificate from the server and updates the config to not use it and disable LDAP. +func (c *Client4) DeleteLdapPublicCertificate() (bool, *Response) { + r, err := c.DoApiDelete(c.GetLdapRoute() + "/certificate/public") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// DeleteLDAPPrivateCertificate deletes the LDAP IDP certificate from the server and updates the config to not use it and disable LDAP. +func (c *Client4) DeleteLdapPrivateCertificate() (bool, *Response) { + r, err := c.DoApiDelete(c.GetLdapRoute() + "/certificate/private") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + // Audits Section // GetAudits returns a list of audits for the whole system. diff --git a/model/config.go b/model/config.go index 7df6da4dfc..ae014727ef 100644 --- a/model/config.go +++ b/model/config.go @@ -1943,9 +1943,11 @@ type LdapSettings struct { SyncIntervalMinutes *int `access:"authentication"` // Advanced - SkipCertificateVerification *bool `access:"authentication"` - QueryTimeout *int `access:"authentication"` - MaxPageSize *int `access:"authentication"` + SkipCertificateVerification *bool `access:"authentication"` + PublicCertificateFile *string `access:"authentication"` + PrivateKeyFile *string `access:"authentication"` + QueryTimeout *int `access:"authentication"` + MaxPageSize *int `access:"authentication"` // Customization LoginFieldName *string `access:"authentication"` @@ -1983,6 +1985,14 @@ func (s *LdapSettings) SetDefaults() { s.ConnectionSecurity = NewString("") } + if s.PublicCertificateFile == nil { + s.PublicCertificateFile = NewString("") + } + + if s.PrivateKeyFile == nil { + s.PrivateKeyFile = NewString("") + } + if s.BaseDN == nil { s.BaseDN = NewString("") } diff --git a/model/ldap.go b/model/ldap.go index d5f98f1a06..4e19c5b1e0 100644 --- a/model/ldap.go +++ b/model/ldap.go @@ -4,5 +4,7 @@ package model const ( - USER_AUTH_SERVICE_LDAP = "ldap" + USER_AUTH_SERVICE_LDAP = "ldap" + LDAP_PUBIC_CERTIFICATE_NAME = "ldap-public.crt" + LDAP_PRIVATE_KEY_NAME = "ldap-private.key" ) diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index cda067a5dd..e34ea28b40 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -606,6 +606,8 @@ func (ts *TelemetryService) trackConfig() { "isempty_guest_filter": isDefault(*cfg.LdapSettings.GuestFilter, ""), "isempty_admin_filter": isDefault(*cfg.LdapSettings.AdminFilter, ""), "isnotempty_picture_attribute": !isDefault(*cfg.LdapSettings.PictureAttribute, ""), + "isnotempty_public_certificate": !isDefault(*cfg.LdapSettings.PublicCertificateFile, ""), + "isnotempty_private_key": !isDefault(*cfg.LdapSettings.PrivateKeyFile, ""), }) ts.sendTelemetry(TRACK_CONFIG_COMPLIANCE, map[string]interface{}{