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 @@
-