diff --git a/api/bolt/migrate_dbversion2.go b/api/bolt/migrate_dbversion2.go new file mode 100644 index 000000000..86059736d --- /dev/null +++ b/api/bolt/migrate_dbversion2.go @@ -0,0 +1,25 @@ +package bolt + +import "github.com/portainer/portainer" + +func (m *Migrator) updateSettingsToVersion3() error { + legacySettings, err := m.SettingsService.Settings() + if err != nil { + return err + } + + legacySettings.AuthenticationMethod = portainer.AuthenticationInternal + legacySettings.LDAPSettings = portainer.LDAPSettings{ + TLSConfig: portainer.TLSConfiguration{}, + SearchSettings: []portainer.LDAPSearchSettings{ + portainer.LDAPSearchSettings{}, + }, + } + + err = m.SettingsService.StoreSettings(legacySettings) + if err != nil { + return err + } + + return nil +} diff --git a/api/bolt/migrator.go b/api/bolt/migrator.go index b6c4dd4df..297afb867 100644 --- a/api/bolt/migrator.go +++ b/api/bolt/migrator.go @@ -7,6 +7,7 @@ type Migrator struct { UserService *UserService EndpointService *EndpointService ResourceControlService *ResourceControlService + SettingsService *SettingsService VersionService *VersionService CurrentDBVersion int store *Store @@ -18,6 +19,7 @@ func NewMigrator(store *Store, version int) *Migrator { UserService: store.UserService, EndpointService: store.EndpointService, ResourceControlService: store.ResourceControlService, + SettingsService: store.SettingsService, VersionService: store.VersionService, CurrentDBVersion: version, store: store, @@ -47,6 +49,14 @@ func (m *Migrator) Migrate() error { } } + // Portainer 1.13.x + if m.CurrentDBVersion == 2 { + err := m.updateSettingsToVersion3() + if err != nil { + return err + } + } + err := m.VersionService.StoreDBVersion(portainer.DBVersion) if err != nil { return err diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index a3e265544..aced452a7 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/portainer/file" "github.com/portainer/portainer/http" "github.com/portainer/portainer/jwt" + "github.com/portainer/portainer/ldap" "log" ) @@ -68,6 +69,10 @@ func initCryptoService() portainer.CryptoService { return &crypto.Service{} } +func initLDAPService() portainer.LDAPService { + return &ldap.Service{} +} + func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool { authorizeEndpointMgmt := true if externalEnpointFile != "" { @@ -113,6 +118,13 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL settings := &portainer.Settings{ LogoURL: *flags.Logo, DisplayExternalContributors: true, + AuthenticationMethod: portainer.AuthenticationInternal, + LDAPSettings: portainer.LDAPSettings{ + TLSConfig: portainer.TLSConfiguration{}, + SearchSettings: []portainer.LDAPSearchSettings{ + portainer.LDAPSearchSettings{}, + }, + }, } if *flags.Templates != "" { @@ -155,6 +167,8 @@ func main() { cryptoService := initCryptoService() + ldapService := initLDAPService() + authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval) err := initSettings(store.SettingsService, flags) @@ -225,6 +239,7 @@ func main() { CryptoService: cryptoService, JWTService: jwtService, FileService: fileService, + LDAPService: ldapService, SSL: *flags.SSL, SSLCert: *flags.SSLCert, SSLKey: *flags.SSLKey, diff --git a/api/crypto/tls.go b/api/crypto/tls.go index ff47d43dc..9c3f1f192 100644 --- a/api/crypto/tls.go +++ b/api/crypto/tls.go @@ -7,20 +7,28 @@ import ( ) // CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key -func CreateTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) { - cert, err := tls.LoadX509KeyPair(certPath, keyPath) - if err != nil { - return nil, err +func CreateTLSConfiguration(caCertPath, certPath, keyPath string, skipTLSVerify bool) (*tls.Config, error) { + + config := &tls.Config{} + + if certPath != "" && keyPath != "" { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, err + } + config.Certificates = []tls.Certificate{cert} } - caCert, err := ioutil.ReadFile(caCertPath) - if err != nil { - return nil, err - } - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(caCert) - config := &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: caCertPool, + + if caCertPath != "" { + caCert, err := ioutil.ReadFile(caCertPath) + if err != nil { + return nil, err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + config.RootCAs = caCertPool } + + config.InsecureSkipVerify = skipTLSVerify return config, nil } diff --git a/api/file/file.go b/api/file/file.go index 8337f3c15..75bcb99ec 100644 --- a/api/file/file.go +++ b/api/file/file.go @@ -6,12 +6,13 @@ import ( "io" "os" "path" - "strconv" ) const ( // TLSStorePath represents the subfolder where TLS files are stored in the file store folder. TLSStorePath = "tls" + // LDAPStorePath represents the subfolder where LDAP TLS files are stored in the TLSStorePath. + LDAPStorePath = "ldap" // TLSCACertFile represents the name on disk for a TLS CA file. TLSCACertFile = "ca.pem" // TLSCertFile represents the name on disk for a TLS certificate file. @@ -50,11 +51,10 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) { return service, nil } -// StoreTLSFile creates a subfolder in the TLSStorePath and stores a new file with the content from r. -func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error { - ID := strconv.Itoa(int(endpointID)) - endpointStorePath := path.Join(TLSStorePath, ID) - err := service.createDirectoryInStoreIfNotExist(endpointStorePath) +// StoreTLSFile creates a folder in the TLSStorePath and stores a new file with the content from r. +func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileType, r io.Reader) error { + storePath := path.Join(TLSStorePath, folder) + err := service.createDirectoryInStoreIfNotExist(storePath) if err != nil { return err } @@ -71,7 +71,7 @@ func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType p return portainer.ErrUndefinedTLSFileType } - tlsFilePath := path.Join(endpointStorePath, fileName) + tlsFilePath := path.Join(storePath, fileName) err = service.createFileInStore(tlsFilePath, r) if err != nil { return err @@ -80,7 +80,7 @@ func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType p } // GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint. -func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType) (string, error) { +func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSFileType) (string, error) { var fileName string switch fileType { case portainer.TLSFileCA: @@ -92,15 +92,13 @@ func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileT default: return "", portainer.ErrUndefinedTLSFileType } - ID := strconv.Itoa(int(endpointID)) - return path.Join(service.fileStorePath, TLSStorePath, ID, fileName), nil + return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil } // DeleteTLSFiles deletes a folder containing the TLS files for an endpoint. -func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error { - ID := strconv.Itoa(int(endpointID)) - endpointPath := path.Join(service.fileStorePath, TLSStorePath, ID) - err := os.RemoveAll(endpointPath) +func (service *Service) DeleteTLSFiles(folder string) error { + storePath := path.Join(service.fileStorePath, TLSStorePath, folder) + err := os.RemoveAll(storePath) if err != nil { return err } diff --git a/api/http/handler/auth.go b/api/http/handler/auth.go index 4c8218282..d6af6597b 100644 --- a/api/http/handler/auth.go +++ b/api/http/handler/auth.go @@ -17,11 +17,13 @@ import ( // AuthHandler represents an HTTP API handler for managing authentication. type AuthHandler struct { *mux.Router - Logger *log.Logger - authDisabled bool - UserService portainer.UserService - CryptoService portainer.CryptoService - JWTService portainer.JWTService + Logger *log.Logger + authDisabled bool + UserService portainer.UserService + CryptoService portainer.CryptoService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + SettingsService portainer.SettingsService } const ( @@ -82,17 +84,32 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques return } - err = handler.CryptoService.CompareHashAndData(u.Password, password) + settings, err := handler.SettingsService.Settings() if err != nil { - httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger) + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return } + if settings.AuthenticationMethod == portainer.AuthenticationLDAP && u.ID != 1 { + err = handler.LDAPService.AuthenticateUser(username, password, &settings.LDAPSettings) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + } else { + err = handler.CryptoService.CompareHashAndData(u.Password, password) + if err != nil { + httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger) + return + } + } + tokenData := &portainer.TokenData{ ID: u.ID, Username: u.Username, Role: u.Role, } + token, err := handler.JWTService.GenerateToken(tokenData) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) diff --git a/api/http/handler/endpoint.go b/api/http/handler/endpoint.go index 7118a9d69..fd8d85598 100644 --- a/api/http/handler/endpoint.go +++ b/api/http/handler/endpoint.go @@ -113,11 +113,12 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht } if req.TLS { - caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA) + folder := strconv.Itoa(int(endpoint.ID)) + caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) endpoint.TLSCACertPath = caCertPath - certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert) + certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) endpoint.TLSCertPath = certPath - keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey) + keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) endpoint.TLSKeyPath = keyPath err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) if err != nil { @@ -272,20 +273,21 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http endpoint.PublicURL = req.PublicURL } + folder := strconv.Itoa(int(endpoint.ID)) if req.TLS { endpoint.TLS = true - caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA) + caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) endpoint.TLSCACertPath = caCertPath - certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert) + certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) endpoint.TLSCertPath = certPath - keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey) + keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) endpoint.TLSKeyPath = keyPath } else { endpoint.TLS = false endpoint.TLSCACertPath = "" endpoint.TLSCertPath = "" endpoint.TLSKeyPath = "" - err = handler.FileService.DeleteTLSFiles(endpoint.ID) + err = handler.FileService.DeleteTLSFiles(folder) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return @@ -347,7 +349,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h } if endpoint.TLS { - err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID)) + err = handler.FileService.DeleteTLSFiles(id) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return diff --git a/api/http/handler/settings.go b/api/http/handler/settings.go index 26e1cfe92..12187625f 100644 --- a/api/http/handler/settings.go +++ b/api/http/handler/settings.go @@ -5,6 +5,7 @@ import ( "github.com/asaskevich/govalidator" "github.com/portainer/portainer" + "github.com/portainer/portainer/file" httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" @@ -20,6 +21,8 @@ type SettingsHandler struct { *mux.Router Logger *log.Logger SettingsService portainer.SettingsService + LDAPService portainer.LDAPService + FileService portainer.FileService } // NewSettingsHandler returns a new instance of OldSettingsHandler. @@ -29,9 +32,13 @@ func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler { Logger: log.New(os.Stderr, "", log.LstdFlags), } h.Handle("/settings", - bouncer.PublicAccess(http.HandlerFunc(h.handleGetSettings))).Methods(http.MethodGet) + bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetSettings))).Methods(http.MethodGet) h.Handle("/settings", bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettings))).Methods(http.MethodPut) + h.Handle("/settings/public", + bouncer.PublicAccess(http.HandlerFunc(h.handleGetPublicSettings))).Methods(http.MethodGet) + h.Handle("/settings/authentication/checkLDAP", + bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettingsLDAPCheck))).Methods(http.MethodPut) return h } @@ -48,6 +55,30 @@ func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http return } +// handleGetPublicSettings handles GET requests on /settings/public +func (handler *SettingsHandler) handleGetPublicSettings(w http.ResponseWriter, r *http.Request) { + settings, err := handler.SettingsService.Settings() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + publicSettings := &publicSettingsResponse{ + LogoURL: settings.LogoURL, + DisplayExternalContributors: settings.DisplayExternalContributors, + AuthenticationMethod: settings.AuthenticationMethod, + } + + encodeJSON(w, publicSettings, handler.Logger) + return +} + +type publicSettingsResponse struct { + LogoURL string `json:"LogoURL"` + DisplayExternalContributors bool `json:"DisplayExternalContributors"` + AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` +} + // handlePutSettings handles PUT requests on /settings func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http.Request) { var req putSettingsRequest @@ -67,6 +98,27 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http LogoURL: req.LogoURL, BlackListedLabels: req.BlackListedLabels, DisplayExternalContributors: req.DisplayExternalContributors, + LDAPSettings: req.LDAPSettings, + } + + if req.AuthenticationMethod == 1 { + settings.AuthenticationMethod = portainer.AuthenticationInternal + } else if req.AuthenticationMethod == 2 { + settings.AuthenticationMethod = portainer.AuthenticationLDAP + } else { + httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + return + } + + if (settings.LDAPSettings.TLSConfig.TLS || settings.LDAPSettings.StartTLS) && !settings.LDAPSettings.TLSConfig.TLSSkipVerify { + caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA) + settings.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath + } else { + settings.LDAPSettings.TLSConfig.TLSCACertPath = "" + err := handler.FileService.DeleteTLSFiles(file.LDAPStorePath) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + } } err = handler.SettingsService.StoreSettings(settings) @@ -76,8 +128,40 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http } type putSettingsRequest struct { - TemplatesURL string `valid:"required"` - LogoURL string `valid:""` - BlackListedLabels []portainer.Pair `valid:""` - DisplayExternalContributors bool `valid:""` + TemplatesURL string `valid:"required"` + LogoURL string `valid:""` + BlackListedLabels []portainer.Pair `valid:""` + DisplayExternalContributors bool `valid:""` + AuthenticationMethod int `valid:"required"` + LDAPSettings portainer.LDAPSettings `valid:""` +} + +// handlePutSettingsLDAPCheck handles PUT requests on /settings/ldap/check +func (handler *SettingsHandler) handlePutSettingsLDAPCheck(w http.ResponseWriter, r *http.Request) { + var req putSettingsLDAPCheckRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) + return + } + + _, err := govalidator.ValidateStruct(req) + if err != nil { + httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + return + } + + if (req.LDAPSettings.TLSConfig.TLS || req.LDAPSettings.StartTLS) && !req.LDAPSettings.TLSConfig.TLSSkipVerify { + caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA) + req.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath + } + + err = handler.LDAPService.TestConnectivity(&req.LDAPSettings) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } +} + +type putSettingsLDAPCheckRequest struct { + LDAPSettings portainer.LDAPSettings `valid:""` } diff --git a/api/http/handler/upload.go b/api/http/handler/upload.go index d96a45c5a..c3d417208 100644 --- a/api/http/handler/upload.go +++ b/api/http/handler/upload.go @@ -8,7 +8,6 @@ import ( "log" "net/http" "os" - "strconv" "github.com/gorilla/mux" ) @@ -26,11 +25,12 @@ func NewUploadHandler(bouncer *security.RequestBouncer) *UploadHandler { Router: mux.NewRouter(), Logger: log.New(os.Stderr, "", log.LstdFlags), } - h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", + h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}", bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUploadTLS))) return h } +// handlePostUploadTLS handles POST requests on /upload/tls/{certificate:(?:ca|cert|key)}?folder=folder func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost}) @@ -38,11 +38,11 @@ func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http } vars := mux.Vars(r) - endpointID := vars["endpointID"] certificate := vars["certificate"] - ID, err := strconv.Atoi(endpointID) - if err != nil { - httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + + folder := r.FormValue("folder") + if folder == "" { + httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger) return } @@ -66,7 +66,7 @@ func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http return } - err = handler.FileService.StoreTLSFile(portainer.EndpointID(ID), fileType, file) + err = handler.FileService.StoreTLSFile(folder, fileType, file) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return diff --git a/api/http/handler/user.go b/api/http/handler/user.go index 44c15495e..2f4079459 100644 --- a/api/http/handler/user.go +++ b/api/http/handler/user.go @@ -26,6 +26,7 @@ type UserHandler struct { TeamMembershipService portainer.TeamMembershipService ResourceControlService portainer.ResourceControlService CryptoService portainer.CryptoService + SettingsService portainer.SettingsService } // NewUserHandler returns a new instance of UserHandler. @@ -93,13 +94,6 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque return } - var role portainer.UserRole - if req.Role == 1 { - role = portainer.AdministratorRole - } else { - role = portainer.StandardUserRole - } - user, err := handler.UserService.UserByUsername(req.Username) if err != nil && err != portainer.ErrUserNotFound { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) @@ -110,16 +104,32 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque return } + var role portainer.UserRole + if req.Role == 1 { + role = portainer.AdministratorRole + } else { + role = portainer.StandardUserRole + } + user = &portainer.User{ Username: req.Username, Role: role, } - user.Password, err = handler.CryptoService.Hash(req.Password) + + settings, err := handler.SettingsService.Settings() if err != nil { - httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger) + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return } + if settings.AuthenticationMethod == portainer.AuthenticationInternal { + user.Password, err = handler.CryptoService.Hash(req.Password) + if err != nil { + httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger) + return + } + } + err = handler.UserService.CreateUser(user) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) @@ -135,7 +145,7 @@ type postUsersResponse struct { type postUsersRequest struct { Username string `valid:"required"` - Password string `valid:"required"` + Password string `valid:""` Role int `valid:"required"` } diff --git a/api/http/handler/websocket.go b/api/http/handler/websocket.go index 39f626a99..dbc4fd9f0 100644 --- a/api/http/handler/websocket.go +++ b/api/http/handler/websocket.go @@ -72,9 +72,7 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) { // Should not be managed here var tlsConfig *tls.Config if endpoint.TLS { - tlsConfig, err = crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, - endpoint.TLSCertPath, - endpoint.TLSKeyPath) + tlsConfig, err = crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false) if err != nil { log.Fatalf("Unable to create TLS configuration: %s", err) return diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go index 3e0d71445..dc733149f 100644 --- a/api/http/proxy/factory.go +++ b/api/http/proxy/factory.go @@ -24,7 +24,7 @@ func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler { func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) { u.Scheme = "https" proxy := factory.createReverseProxy(u) - config, err := crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath) + config, err := crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false) if err != nil { return nil, err } diff --git a/api/http/server.go b/api/http/server.go index 14a069eae..36344f764 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -27,6 +27,7 @@ type Server struct { FileService portainer.FileService RegistryService portainer.RegistryService DockerHubService portainer.DockerHubService + LDAPService portainer.LDAPService Handler *handler.Handler SSL bool SSLCert string @@ -42,12 +43,15 @@ func (server *Server) Start() error { authHandler.UserService = server.UserService authHandler.CryptoService = server.CryptoService authHandler.JWTService = server.JWTService + authHandler.LDAPService = server.LDAPService + authHandler.SettingsService = server.SettingsService var userHandler = handler.NewUserHandler(requestBouncer) userHandler.UserService = server.UserService userHandler.TeamService = server.TeamService userHandler.TeamMembershipService = server.TeamMembershipService userHandler.CryptoService = server.CryptoService userHandler.ResourceControlService = server.ResourceControlService + userHandler.SettingsService = server.SettingsService var teamHandler = handler.NewTeamHandler(requestBouncer) teamHandler.TeamService = server.TeamService teamHandler.TeamMembershipService = server.TeamMembershipService @@ -56,6 +60,8 @@ func (server *Server) Start() error { var statusHandler = handler.NewStatusHandler(requestBouncer, server.Status) var settingsHandler = handler.NewSettingsHandler(requestBouncer) settingsHandler.SettingsService = server.SettingsService + settingsHandler.LDAPService = server.LDAPService + settingsHandler.FileService = server.FileService var templatesHandler = handler.NewTemplatesHandler(requestBouncer) templatesHandler.SettingsService = server.SettingsService var dockerHandler = handler.NewDockerHandler(requestBouncer) diff --git a/api/ldap/ldap.go b/api/ldap/ldap.go new file mode 100644 index 000000000..786332c35 --- /dev/null +++ b/api/ldap/ldap.go @@ -0,0 +1,123 @@ +package ldap + +import ( + "fmt" + "strings" + + "github.com/portainer/portainer" + "github.com/portainer/portainer/crypto" + + "gopkg.in/ldap.v2" +) + +const ( + // ErrUserNotFound defines an error raised when the user is not found via LDAP search + // or that too many entries (> 1) are returned. + ErrUserNotFound = portainer.Error("User not found or too many entries returned") +) + +// Service represents a service used to authenticate users against a LDAP/AD. +type Service struct{} + +func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) { + var userDN string + found := false + for _, searchSettings := range settings { + searchRequest := ldap.NewSearchRequest( + searchSettings.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, username), + []string{"dn"}, + nil, + ) + + // Deliberately skip errors on the search request so that we can jump to other search settings + // if any issue arise with the current one. + sr, _ := conn.Search(searchRequest) + + if len(sr.Entries) == 1 { + found = true + userDN = sr.Entries[0].DN + break + } + } + + if !found { + return "", ErrUserNotFound + } + + return userDN, nil +} + +func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) { + + if settings.TLSConfig.TLS || settings.StartTLS { + config, err := crypto.CreateTLSConfiguration(settings.TLSConfig.TLSCACertPath, "", "", settings.TLSConfig.TLSSkipVerify) + if err != nil { + return nil, err + } + config.ServerName = strings.Split(settings.URL, ":")[0] + + if settings.TLSConfig.TLS { + return ldap.DialTLS("tcp", settings.URL, config) + } + + conn, err := ldap.Dial("tcp", settings.URL) + if err != nil { + return nil, err + } + + err = conn.StartTLS(config) + if err != nil { + return nil, err + } + + return conn, nil + } + + return ldap.Dial("tcp", settings.URL) +} + +// AuthenticateUser is used to authenticate a user against a LDAP/AD. +func (*Service) AuthenticateUser(username, password string, settings *portainer.LDAPSettings) error { + + connection, err := createConnection(settings) + if err != nil { + return err + } + defer connection.Close() + + err = connection.Bind(settings.ReaderDN, settings.Password) + if err != nil { + return err + } + + userDN, err := searchUser(username, connection, settings.SearchSettings) + if err != nil { + return err + } + + err = connection.Bind(userDN, password) + if err != nil { + return err + } + + return nil +} + +// TestConnectivity is used to test a connection against the LDAP server using the credentials +// specified in the LDAPSettings. +func (*Service) TestConnectivity(settings *portainer.LDAPSettings) error { + + connection, err := createConnection(settings) + if err != nil { + return err + } + defer connection.Close() + + err = connection.Bind(settings.ReaderDN, settings.Password) + if err != nil { + return err + } + return nil +} diff --git a/api/portainer.go b/api/portainer.go index 6ed70e7ee..91770faf4 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -41,12 +41,40 @@ type ( Version string `json:"Version"` } + // LDAPSettings represents the settings used to connect to a LDAP server. + LDAPSettings struct { + ReaderDN string `json:"ReaderDN"` + Password string `json:"Password"` + URL string `json:"URL"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + StartTLS bool `json:"StartTLS"` + SearchSettings []LDAPSearchSettings `json:"SearchSettings"` + } + + // TLSConfiguration represents a TLS configuration. + TLSConfiguration struct { + TLS bool `json:"TLS"` + TLSSkipVerify bool `json:"TLSSkipVerify"` + TLSCACertPath string `json:"TLSCACert,omitempty"` + TLSCertPath string `json:"TLSCert,omitempty"` + TLSKeyPath string `json:"TLSKey,omitempty"` + } + + // LDAPSearchSettings represents settings used to search for users in a LDAP server. + LDAPSearchSettings struct { + BaseDN string `json:"BaseDN"` + Filter string `json:"Filter"` + UserNameAttribute string `json:"UserNameAttribute"` + } + // Settings represents the application settings. Settings struct { - TemplatesURL string `json:"TemplatesURL"` - LogoURL string `json:"LogoURL"` - BlackListedLabels []Pair `json:"BlackListedLabels"` - DisplayExternalContributors bool `json:"DisplayExternalContributors"` + TemplatesURL string `json:"TemplatesURL"` + LogoURL string `json:"LogoURL"` + BlackListedLabels []Pair `json:"BlackListedLabels"` + DisplayExternalContributors bool `json:"DisplayExternalContributors"` + AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` + LDAPSettings LDAPSettings `json:"LDAPSettings"` } // User represents a user account. @@ -64,6 +92,9 @@ type ( // or a regular user UserRole int + // AuthenticationMethod represents the authentication method used to authenticate a user. + AuthenticationMethod int + // Team represents a list of user accounts. Team struct { ID TeamID `json:"Id"` @@ -292,22 +323,28 @@ type ( // FileService represents a service for managing files. FileService interface { - StoreTLSFile(endpointID EndpointID, fileType TLSFileType, r io.Reader) error - GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error) - DeleteTLSFiles(endpointID EndpointID) error + StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error + GetPathForTLSFile(folder string, fileType TLSFileType) (string, error) + DeleteTLSFiles(folder string) error } // EndpointWatcher represents a service to synchronize the endpoints via an external source. EndpointWatcher interface { WatchEndpointFile(endpointFilePath string) error } + + // LDAPService represents a service used to authenticate users against a LDAP/AD. + LDAPService interface { + AuthenticateUser(username, password string, settings *LDAPSettings) error + TestConnectivity(settings *LDAPSettings) error + } ) const ( // APIVersion is the version number of the Portainer API. APIVersion = "1.13.6" // DBVersion is the version number of the Portainer database. - DBVersion = 2 + DBVersion = 3 // DefaultTemplatesURL represents the default URL for the templates definitions. DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json" ) @@ -337,6 +374,14 @@ const ( StandardUserRole ) +const ( + _ AuthenticationMethod = iota + // AuthenticationInternal represents the internal authentication method (authentication against Portainer API) + AuthenticationInternal + // AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server) + AuthenticationLDAP +) + const ( _ ResourceAccessLevel = iota // ReadWriteAccessLevel represents an access level with read-write permissions on a resource diff --git a/app/app.js b/app/app.js index 7598aff3b..eb80ea85b 100644 --- a/app/app.js +++ b/app/app.js @@ -51,6 +51,7 @@ angular.module('portainer', [ 'service', 'services', 'settings', + 'settingsAuthentication', 'sidebar', 'stats', 'swarm', @@ -563,6 +564,19 @@ angular.module('portainer', [ } } }) + .state('settings_authentication', { + url: '^/settings/authentication', + views: { + 'content@': { + templateUrl: 'app/components/settingsAuthentication/settingsAuthentication.html', + controller: 'SettingsAuthenticationController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) .state('task', { url: '^/task/:id', views: { diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index 356f244f5..f633c0ed8 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -98,7 +98,7 @@ - +
diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index 94aa92794..d34ad9bd1 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -101,7 +101,7 @@
- +
diff --git a/app/components/createVolume/createvolume.html b/app/components/createVolume/createvolume.html index 2ca08ec84..c9e795dd7 100644 --- a/app/components/createVolume/createvolume.html +++ b/app/components/createVolume/createvolume.html @@ -65,7 +65,7 @@
- +
diff --git a/app/components/settingsAuthentication/settingsAuthentication.html b/app/components/settingsAuthentication/settingsAuthentication.html new file mode 100644 index 000000000..6c94043f8 --- /dev/null +++ b/app/components/settingsAuthentication/settingsAuthentication.html @@ -0,0 +1,254 @@ + + + + + + Settings > Authentication + + + +
+
+ + + +
+
+ Authentication method +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ Information +
+
+ + When using internal authentication, Portainer will encrypt user passwords and store credentials locally. + +
+
+ + When using LDAP authentication, Portainer will delegate user authentication to a LDAP server (exception for the admin user that always use internal authentication). +

+ + Users still need to be created in Portainer beforehand. +

+
+
+ +
+
+ LDAP configuration +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ LDAP security +
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+ +
+ +
+ + + {{ formValues.TLSCACert.name }} + + + + +
+
+ +
+ + +
+ +
+ + +
+
+ +
+ User search configurations +
+ + +
+ +
+ + Extra search configuration + +
+ +
+ +
+ +
+ + +
+ +
+
+
+ +
+ +
+
+ +
+
+ +
+ + add search configuration + +
+ +
+ +
+ + +
+
+ + + +
+
+ + +
+
+
+
+
diff --git a/app/components/settingsAuthentication/settingsAuthenticationController.js b/app/components/settingsAuthentication/settingsAuthenticationController.js new file mode 100644 index 000000000..faf16873b --- /dev/null +++ b/app/components/settingsAuthentication/settingsAuthenticationController.js @@ -0,0 +1,93 @@ +angular.module('settingsAuthentication', []) +.controller('SettingsAuthenticationController', ['$q', '$scope', 'Notifications', 'SettingsService', 'FileUploadService', +function ($q, $scope, Notifications, SettingsService, FileUploadService) { + + $scope.state = { + successfulConnectivityCheck: false, + failedConnectivityCheck: false, + uploadInProgress: false + }; + + $scope.formValues = { + TLSCACert: '' + }; + + $scope.addSearchConfiguration = function() { + $scope.LDAPSettings.SearchSettings.push({ BaseDN: '', UserNameAttribute: '', Filter: '' }); + }; + + $scope.removeSearchConfiguration = function(index) { + $scope.LDAPSettings.SearchSettings.splice(index, 1); + }; + + $scope.LDAPConnectivityCheck = function() { + $('#connectivityCheckSpinner').show(); + var settings = $scope.settings; + var TLSCAFile = $scope.formValues.TLSCACert !== settings.LDAPSettings.TLSConfig.TLSCACert ? $scope.formValues.TLSCACert : null; + + var uploadRequired = ($scope.LDAPSettings.TLSConfig.TLS || $scope.LDAPSettings.StartTLS) && !$scope.LDAPSettings.TLSConfig.TLSSkipVerify; + $scope.state.uploadInProgress = uploadRequired; + + $q.when(!uploadRequired || FileUploadService.uploadLDAPTLSFiles(TLSCAFile, null, null)) + .then(function success(data) { + return SettingsService.checkLDAPConnectivity(settings); + }) + .then(function success(data) { + $scope.state.failedConnectivityCheck = false; + $scope.state.successfulConnectivityCheck = true; + Notifications.success('Connection to LDAP successful'); + }) + .catch(function error(err) { + $scope.state.failedConnectivityCheck = true; + $scope.state.successfulConnectivityCheck = false; + Notifications.error('Failure', err, 'Connection to LDAP failed'); + }) + .finally(function final() { + $scope.state.uploadInProgress = false; + $('#connectivityCheckSpinner').hide(); + }); + }; + + $scope.saveSettings = function() { + $('#updateSettingsSpinner').show(); + var settings = $scope.settings; + var TLSCAFile = $scope.formValues.TLSCACert !== settings.LDAPSettings.TLSConfig.TLSCACert ? $scope.formValues.TLSCACert : null; + + var uploadRequired = ($scope.LDAPSettings.TLSConfig.TLS || $scope.LDAPSettings.StartTLS) && !$scope.LDAPSettings.TLSConfig.TLSSkipVerify; + $scope.state.uploadInProgress = uploadRequired; + + $q.when(!uploadRequired || FileUploadService.uploadLDAPTLSFiles(TLSCAFile, null, null)) + .then(function success(data) { + return SettingsService.update(settings); + }) + .then(function success(data) { + Notifications.success('Authentication settings updated'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update authentication settings'); + }) + .finally(function final() { + $scope.state.uploadInProgress = false; + $('#updateSettingsSpinner').hide(); + }); + }; + + function initView() { + $('#loadingViewSpinner').show(); + SettingsService.settings() + .then(function success(data) { + var settings = data; + $scope.settings = settings; + $scope.LDAPSettings = settings.LDAPSettings; + $scope.formValues.TLSCACert = settings.LDAPSettings.TLSConfig.TLSCACert; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve application settings'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); +}]); diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index 3d8d6a2c3..2f4c79424 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -69,6 +69,9 @@ - +
diff --git a/app/components/user/user.html b/app/components/user/user.html index 62f0a1e75..b1a6d3478 100644 --- a/app/components/user/user.html +++ b/app/components/user/user.html @@ -32,29 +32,6 @@ - @@ -62,7 +39,7 @@
-
+
diff --git a/app/components/user/userController.js b/app/components/user/userController.js index 348457c51..dfb3c0489 100644 --- a/app/components/user/userController.js +++ b/app/components/user/userController.js @@ -1,6 +1,6 @@ angular.module('user', []) -.controller('UserController', ['$q', '$scope', '$state', '$stateParams', 'UserService', 'ModalService', 'Notifications', -function ($q, $scope, $state, $stateParams, UserService, ModalService, Notifications) { +.controller('UserController', ['$q', '$scope', '$state', '$stateParams', 'UserService', 'ModalService', 'Notifications', 'SettingsService', +function ($q, $scope, $state, $stateParams, UserService, ModalService, Notifications, SettingsService) { $scope.state = { updatePasswordError: '' @@ -72,12 +72,14 @@ function ($q, $scope, $state, $stateParams, UserService, ModalService, Notificat function initView() { $('#loadingViewSpinner').show(); $q.all({ - user: UserService.user($stateParams.id) + user: UserService.user($stateParams.id), + settings: SettingsService.publicSettings() }) .then(function success(data) { var user = data.user; $scope.user = user; $scope.formValues.Administrator = user.Role === 1 ? true : false; + $scope.AuthenticationMethod = data.settings.AuthenticationMethod; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve user information'); diff --git a/app/components/userSettings/userSettings.html b/app/components/userSettings/userSettings.html index 8a20d22f8..c7f5405f7 100644 --- a/app/components/userSettings/userSettings.html +++ b/app/components/userSettings/userSettings.html @@ -1,5 +1,6 @@ + User settings @@ -58,7 +59,11 @@
- + + + + You cannot change your password when using LDAP authentication. +
diff --git a/app/components/userSettings/userSettingsController.js b/app/components/userSettings/userSettingsController.js index d8e8f4d43..2146d58e0 100644 --- a/app/components/userSettings/userSettingsController.js +++ b/app/components/userSettings/userSettingsController.js @@ -1,6 +1,6 @@ angular.module('userSettings', []) -.controller('UserSettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications', -function ($scope, $state, $sanitize, Authentication, UserService, Notifications) { +.controller('UserSettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications', 'SettingsService', +function ($scope, $state, $sanitize, Authentication, UserService, Notifications, SettingsService) { $scope.formValues = { currentPassword: '', newPassword: '', @@ -26,4 +26,19 @@ function ($scope, $state, $sanitize, Authentication, UserService, Notifications) } }); }; + + function initView() { + SettingsService.publicSettings() + .then(function success(data) { + $scope.AuthenticationMethod = data.AuthenticationMethod; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve application settings'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); }]); diff --git a/app/components/users/users.html b/app/components/users/users.html index 68e0ef9ea..79c9999d3 100644 --- a/app/components/users/users.html +++ b/app/components/users/users.html @@ -17,7 +17,10 @@
- +
@@ -27,8 +30,8 @@
-
- +
+
@@ -38,8 +41,8 @@
-
- +
+
@@ -95,7 +98,7 @@
- + {{ state.userCreationError }} @@ -140,19 +143,26 @@ - + Name - + Role + + + Authentication + + + + @@ -166,6 +176,10 @@ {{ user.RoleName }} + + Internal + LDAP + Edit diff --git a/app/components/users/usersController.js b/app/components/users/usersController.js index bc595c2f6..0184e84f0 100644 --- a/app/components/users/usersController.js +++ b/app/components/users/usersController.js @@ -1,6 +1,6 @@ angular.module('users', []) -.controller('UsersController', ['$q', '$scope', '$state', '$sanitize', 'UserService', 'TeamService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication', -function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication) { +.controller('UsersController', ['$q', '$scope', '$state', '$sanitize', 'UserService', 'TeamService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication', 'SettingsService', +function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication, SettingsService) { $scope.state = { userCreationError: '', selectedItemCount: 0, @@ -140,13 +140,15 @@ function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershi $q.all({ users: UserService.users(true), teams: isAdmin ? TeamService.teams() : UserService.userLeadingTeams(userDetails.ID), - memberships: TeamMembershipService.memberships() + memberships: TeamMembershipService.memberships(), + settings: SettingsService.publicSettings() }) .then(function success(data) { var users = data.users; assignTeamLeaders(users, data.memberships); $scope.users = users; $scope.teams = data.teams; + $scope.AuthenticationMethod = data.settings.AuthenticationMethod; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve users and teams'); diff --git a/app/models/api/settings/ldapSettings.js b/app/models/api/settings/ldapSettings.js new file mode 100644 index 000000000..2da574598 --- /dev/null +++ b/app/models/api/settings/ldapSettings.js @@ -0,0 +1,12 @@ +function LDAPSettingsViewModel(data) { + this.ReaderDN = data.ReaderDN; + this.Password = data.Password; + this.URL = data.URL; + this.SearchSettings = data.SearchSettings; +} + +function LDAPSearchSettings(BaseDN, UsernameAttribute, Filter) { + this.BaseDN = BaseDN; + this.UsernameAttribute = UsernameAttribute; + this.Filter = Filter; +} diff --git a/app/models/api/settings.js b/app/models/api/settings/settings.js similarity index 70% rename from app/models/api/settings.js rename to app/models/api/settings/settings.js index 889d18ad1..b8d473172 100644 --- a/app/models/api/settings.js +++ b/app/models/api/settings/settings.js @@ -3,4 +3,6 @@ function SettingsViewModel(data) { this.LogoURL = data.LogoURL; this.BlackListedLabels = data.BlackListedLabels; this.DisplayExternalContributors = data.DisplayExternalContributors; + this.AuthenticationMethod = data.AuthenticationMethod; + this.LDAPSettings = data.LDAPSettings; } diff --git a/app/models/api/user.js b/app/models/api/user.js index 1177fc137..27afa4b67 100644 --- a/app/models/api/user.js +++ b/app/models/api/user.js @@ -7,5 +7,6 @@ function UserViewModel(data) { } else { this.RoleName = 'user'; } + this.AuthenticationMethod = data.AuthenticationMethod; this.Checked = false; } diff --git a/app/rest/api/settings.js b/app/rest/api/settings.js index 46ee8f336..9a70d8885 100644 --- a/app/rest/api/settings.js +++ b/app/rest/api/settings.js @@ -1,8 +1,10 @@ angular.module('portainer.rest') .factory('Settings', ['$resource', 'API_ENDPOINT_SETTINGS', function SettingsFactory($resource, API_ENDPOINT_SETTINGS) { 'use strict'; - return $resource(API_ENDPOINT_SETTINGS, {}, { + return $resource(API_ENDPOINT_SETTINGS + '/:subResource/:action', {}, { get: { method: 'GET' }, - update: { method: 'PUT' } + update: { method: 'PUT' }, + publicSettings: { method: 'GET', params: { subResource: 'public' } }, + checkLDAPConnectivity: { method: 'PUT', params: { subResource: 'authentication', action: 'checkLDAP' } } }); }]); diff --git a/app/services/api/settingsService.js b/app/services/api/settingsService.js index f467ff844..4455abb97 100644 --- a/app/services/api/settingsService.js +++ b/app/services/api/settingsService.js @@ -22,5 +22,24 @@ angular.module('portainer.services') return Settings.update({}, settings).$promise; }; + service.publicSettings = function() { + var deferred = $q.defer(); + + Settings.publicSettings().$promise + .then(function success(data) { + var settings = new SettingsViewModel(data); + deferred.resolve(settings); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve application settings', err: err }); + }); + + return deferred.promise; + }; + + service.checkLDAPConnectivity = function(settings) { + return Settings.checkLDAPConnectivity({}, settings).$promise; + }; + return service; }]); diff --git a/app/services/fileUpload.js b/app/services/fileUpload.js index 7811afed4..8b256ca00 100644 --- a/app/services/fileUpload.js +++ b/app/services/fileUpload.js @@ -1,44 +1,44 @@ angular.module('portainer.services') .factory('FileUploadService', ['$q', 'Upload', function FileUploadFactory($q, Upload) { 'use strict'; - function uploadFile(url, file) { - var deferred = $q.defer(); - Upload.upload({ - url: url, - data: { file: file } - }).then(function success(data) { - deferred.resolve(data); - }, function error(e) { - deferred.reject(e); - }, function progress(evt) { - }); - return deferred.promise; - } - return { - uploadTLSFilesForEndpoint: function(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) { - var deferred = $q.defer(); - var queue = []; - if (TLSCAFile) { - var uploadTLSCA = uploadFile('api/upload/tls/' + endpointID + '/ca', TLSCAFile); - queue.push(uploadTLSCA); - } - if (TLSCertFile) { - var uploadTLSCert = uploadFile('api/upload/tls/' + endpointID + '/cert', TLSCertFile); - queue.push(uploadTLSCert); - } - if (TLSKeyFile) { - var uploadTLSKey = uploadFile('api/upload/tls/' + endpointID + '/key', TLSKeyFile); - queue.push(uploadTLSKey); - } - $q.all(queue).then(function (data) { - deferred.resolve(data); - }, function (err) { - deferred.reject(err); - }, function update(evt) { - deferred.notify(evt); - }); - return deferred.promise; + var service = {}; + + function uploadFile(url, file) { + return Upload.upload({ url: url, data: { file: file }}); + } + + service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) { + var queue = []; + + if (TLSCAFile) { + queue.push(uploadFile('api/upload/tls/ca?folder=ldap', TLSCAFile)); } + if (TLSCertFile) { + queue.push(uploadFile('api/upload/tls/cert?folder=ldap', TLSCertFile)); + } + if (TLSKeyFile) { + queue.push(uploadFile('api/upload/tls/key?folder=ldap', TLSKeyFile)); + } + + return $q.all(queue); }; + + service.uploadTLSFilesForEndpoint = function(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) { + var queue = []; + + if (TLSCAFile) { + queue.push(uploadFile('api/upload/tls/ca?folder=' + endpointID, TLSCAFile)); + } + if (TLSCertFile) { + queue.push(uploadFile('api/upload/tls/cert?folder=' + endpointID, TLSCertFile)); + } + if (TLSKeyFile) { + queue.push(uploadFile('api/upload/tls/key?folder=' + endpointID, TLSKeyFile)); + } + + return $q.all(queue); + }; + + return service; }]); diff --git a/app/services/stateManager.js b/app/services/stateManager.js index ae7e7838e..9385237a7 100644 --- a/app/services/stateManager.js +++ b/app/services/stateManager.js @@ -44,7 +44,7 @@ angular.module('portainer.services') deferred.resolve(state); } else { $q.all({ - settings: SettingsService.settings(), + settings: SettingsService.publicSettings(), status: StatusService.status() }) .then(function success(data) { diff --git a/assets/css/app.css b/assets/css/app.css index e9b61a130..2b2f4bb80 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -82,10 +82,6 @@ a[ng-click]{ margin-right: 5px; } -.fa.green-icon { - color: #23ae89; -} - .tooltip.portainer-tooltip .tooltip-inner { font-family: Montserrat; background-color: #ffffff; @@ -106,6 +102,10 @@ a[ng-click]{ color: #337ab7; } +.fa.green-icon { + color: #23ae89; +} + .fa.red-icon { color: #ae2323; } @@ -517,4 +517,4 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { .monospaced { font-family: monospace; font-weight: 600; -} \ No newline at end of file +}