mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +02:00
feat(authentication): add LDAP authentication support (#1093)
This commit is contained in:
parent
04ea81e7cd
commit
d27528a771
37 changed files with 922 additions and 166 deletions
25
api/bolt/migrate_dbversion2.go
Normal file
25
api/bolt/migrate_dbversion2.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ type Migrator struct {
|
||||||
UserService *UserService
|
UserService *UserService
|
||||||
EndpointService *EndpointService
|
EndpointService *EndpointService
|
||||||
ResourceControlService *ResourceControlService
|
ResourceControlService *ResourceControlService
|
||||||
|
SettingsService *SettingsService
|
||||||
VersionService *VersionService
|
VersionService *VersionService
|
||||||
CurrentDBVersion int
|
CurrentDBVersion int
|
||||||
store *Store
|
store *Store
|
||||||
|
@ -18,6 +19,7 @@ func NewMigrator(store *Store, version int) *Migrator {
|
||||||
UserService: store.UserService,
|
UserService: store.UserService,
|
||||||
EndpointService: store.EndpointService,
|
EndpointService: store.EndpointService,
|
||||||
ResourceControlService: store.ResourceControlService,
|
ResourceControlService: store.ResourceControlService,
|
||||||
|
SettingsService: store.SettingsService,
|
||||||
VersionService: store.VersionService,
|
VersionService: store.VersionService,
|
||||||
CurrentDBVersion: version,
|
CurrentDBVersion: version,
|
||||||
store: store,
|
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)
|
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/portainer/portainer/file"
|
"github.com/portainer/portainer/file"
|
||||||
"github.com/portainer/portainer/http"
|
"github.com/portainer/portainer/http"
|
||||||
"github.com/portainer/portainer/jwt"
|
"github.com/portainer/portainer/jwt"
|
||||||
|
"github.com/portainer/portainer/ldap"
|
||||||
|
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
@ -68,6 +69,10 @@ func initCryptoService() portainer.CryptoService {
|
||||||
return &crypto.Service{}
|
return &crypto.Service{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initLDAPService() portainer.LDAPService {
|
||||||
|
return &ldap.Service{}
|
||||||
|
}
|
||||||
|
|
||||||
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
|
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
|
||||||
authorizeEndpointMgmt := true
|
authorizeEndpointMgmt := true
|
||||||
if externalEnpointFile != "" {
|
if externalEnpointFile != "" {
|
||||||
|
@ -113,6 +118,13 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||||
settings := &portainer.Settings{
|
settings := &portainer.Settings{
|
||||||
LogoURL: *flags.Logo,
|
LogoURL: *flags.Logo,
|
||||||
DisplayExternalContributors: true,
|
DisplayExternalContributors: true,
|
||||||
|
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||||
|
LDAPSettings: portainer.LDAPSettings{
|
||||||
|
TLSConfig: portainer.TLSConfiguration{},
|
||||||
|
SearchSettings: []portainer.LDAPSearchSettings{
|
||||||
|
portainer.LDAPSearchSettings{},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.Templates != "" {
|
if *flags.Templates != "" {
|
||||||
|
@ -155,6 +167,8 @@ func main() {
|
||||||
|
|
||||||
cryptoService := initCryptoService()
|
cryptoService := initCryptoService()
|
||||||
|
|
||||||
|
ldapService := initLDAPService()
|
||||||
|
|
||||||
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
|
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
|
||||||
|
|
||||||
err := initSettings(store.SettingsService, flags)
|
err := initSettings(store.SettingsService, flags)
|
||||||
|
@ -225,6 +239,7 @@ func main() {
|
||||||
CryptoService: cryptoService,
|
CryptoService: cryptoService,
|
||||||
JWTService: jwtService,
|
JWTService: jwtService,
|
||||||
FileService: fileService,
|
FileService: fileService,
|
||||||
|
LDAPService: ldapService,
|
||||||
SSL: *flags.SSL,
|
SSL: *flags.SSL,
|
||||||
SSLCert: *flags.SSLCert,
|
SSLCert: *flags.SSLCert,
|
||||||
SSLKey: *flags.SSLKey,
|
SSLKey: *flags.SSLKey,
|
||||||
|
|
|
@ -7,20 +7,28 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
||||||
func CreateTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
|
func CreateTLSConfiguration(caCertPath, certPath, keyPath string, skipTLSVerify bool) (*tls.Config, error) {
|
||||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
|
||||||
if err != nil {
|
config := &tls.Config{}
|
||||||
return nil, err
|
|
||||||
|
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 {
|
if caCertPath != "" {
|
||||||
return nil, err
|
caCert, err := ioutil.ReadFile(caCertPath)
|
||||||
}
|
if err != nil {
|
||||||
caCertPool := x509.NewCertPool()
|
return nil, err
|
||||||
caCertPool.AppendCertsFromPEM(caCert)
|
}
|
||||||
config := &tls.Config{
|
caCertPool := x509.NewCertPool()
|
||||||
Certificates: []tls.Certificate{cert},
|
caCertPool.AppendCertsFromPEM(caCert)
|
||||||
RootCAs: caCertPool,
|
config.RootCAs = caCertPool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.InsecureSkipVerify = skipTLSVerify
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,13 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TLSStorePath represents the subfolder where TLS files are stored in the file store folder.
|
// TLSStorePath represents the subfolder where TLS files are stored in the file store folder.
|
||||||
TLSStorePath = "tls"
|
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 represents the name on disk for a TLS CA file.
|
||||||
TLSCACertFile = "ca.pem"
|
TLSCACertFile = "ca.pem"
|
||||||
// TLSCertFile represents the name on disk for a TLS certificate file.
|
// 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
|
return service, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoreTLSFile creates a subfolder in the TLSStorePath and stores a new file with the content from r.
|
// StoreTLSFile creates a folder 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 {
|
func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileType, r io.Reader) error {
|
||||||
ID := strconv.Itoa(int(endpointID))
|
storePath := path.Join(TLSStorePath, folder)
|
||||||
endpointStorePath := path.Join(TLSStorePath, ID)
|
err := service.createDirectoryInStoreIfNotExist(storePath)
|
||||||
err := service.createDirectoryInStoreIfNotExist(endpointStorePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType p
|
||||||
return portainer.ErrUndefinedTLSFileType
|
return portainer.ErrUndefinedTLSFileType
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsFilePath := path.Join(endpointStorePath, fileName)
|
tlsFilePath := path.Join(storePath, fileName)
|
||||||
err = service.createFileInStore(tlsFilePath, r)
|
err = service.createFileInStore(tlsFilePath, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// 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
|
var fileName string
|
||||||
switch fileType {
|
switch fileType {
|
||||||
case portainer.TLSFileCA:
|
case portainer.TLSFileCA:
|
||||||
|
@ -92,15 +92,13 @@ func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileT
|
||||||
default:
|
default:
|
||||||
return "", portainer.ErrUndefinedTLSFileType
|
return "", portainer.ErrUndefinedTLSFileType
|
||||||
}
|
}
|
||||||
ID := strconv.Itoa(int(endpointID))
|
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
|
||||||
return path.Join(service.fileStorePath, TLSStorePath, ID, fileName), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint.
|
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint.
|
||||||
func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error {
|
func (service *Service) DeleteTLSFiles(folder string) error {
|
||||||
ID := strconv.Itoa(int(endpointID))
|
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
|
||||||
endpointPath := path.Join(service.fileStorePath, TLSStorePath, ID)
|
err := os.RemoveAll(storePath)
|
||||||
err := os.RemoveAll(endpointPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,13 @@ import (
|
||||||
// AuthHandler represents an HTTP API handler for managing authentication.
|
// AuthHandler represents an HTTP API handler for managing authentication.
|
||||||
type AuthHandler struct {
|
type AuthHandler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
authDisabled bool
|
authDisabled bool
|
||||||
UserService portainer.UserService
|
UserService portainer.UserService
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
JWTService portainer.JWTService
|
JWTService portainer.JWTService
|
||||||
|
LDAPService portainer.LDAPService
|
||||||
|
SettingsService portainer.SettingsService
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -82,17 +84,32 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
settings, err := handler.SettingsService.Settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
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{
|
tokenData := &portainer.TokenData{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
Role: u.Role,
|
Role: u.Role,
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
|
|
@ -113,11 +113,12 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.TLS {
|
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
|
endpoint.TLSCACertPath = caCertPath
|
||||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||||
endpoint.TLSCertPath = certPath
|
endpoint.TLSCertPath = certPath
|
||||||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||||
endpoint.TLSKeyPath = keyPath
|
endpoint.TLSKeyPath = keyPath
|
||||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -272,20 +273,21 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
||||||
endpoint.PublicURL = req.PublicURL
|
endpoint.PublicURL = req.PublicURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
folder := strconv.Itoa(int(endpoint.ID))
|
||||||
if req.TLS {
|
if req.TLS {
|
||||||
endpoint.TLS = true
|
endpoint.TLS = true
|
||||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||||
endpoint.TLSCACertPath = caCertPath
|
endpoint.TLSCACertPath = caCertPath
|
||||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||||
endpoint.TLSCertPath = certPath
|
endpoint.TLSCertPath = certPath
|
||||||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||||
endpoint.TLSKeyPath = keyPath
|
endpoint.TLSKeyPath = keyPath
|
||||||
} else {
|
} else {
|
||||||
endpoint.TLS = false
|
endpoint.TLS = false
|
||||||
endpoint.TLSCACertPath = ""
|
endpoint.TLSCACertPath = ""
|
||||||
endpoint.TLSCertPath = ""
|
endpoint.TLSCertPath = ""
|
||||||
endpoint.TLSKeyPath = ""
|
endpoint.TLSKeyPath = ""
|
||||||
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
|
err = handler.FileService.DeleteTLSFiles(folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
@ -347,7 +349,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpoint.TLS {
|
if endpoint.TLS {
|
||||||
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
|
err = handler.FileService.DeleteTLSFiles(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
"github.com/portainer/portainer/file"
|
||||||
httperror "github.com/portainer/portainer/http/error"
|
httperror "github.com/portainer/portainer/http/error"
|
||||||
"github.com/portainer/portainer/http/security"
|
"github.com/portainer/portainer/http/security"
|
||||||
|
|
||||||
|
@ -20,6 +21,8 @@ type SettingsHandler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
|
LDAPService portainer.LDAPService
|
||||||
|
FileService portainer.FileService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSettingsHandler returns a new instance of OldSettingsHandler.
|
// NewSettingsHandler returns a new instance of OldSettingsHandler.
|
||||||
|
@ -29,9 +32,13 @@ func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler {
|
||||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
}
|
}
|
||||||
h.Handle("/settings",
|
h.Handle("/settings",
|
||||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetSettings))).Methods(http.MethodGet)
|
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetSettings))).Methods(http.MethodGet)
|
||||||
h.Handle("/settings",
|
h.Handle("/settings",
|
||||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettings))).Methods(http.MethodPut)
|
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
|
return h
|
||||||
}
|
}
|
||||||
|
@ -48,6 +55,30 @@ func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http
|
||||||
return
|
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
|
// handlePutSettings handles PUT requests on /settings
|
||||||
func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http.Request) {
|
func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
var req putSettingsRequest
|
var req putSettingsRequest
|
||||||
|
@ -67,6 +98,27 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http
|
||||||
LogoURL: req.LogoURL,
|
LogoURL: req.LogoURL,
|
||||||
BlackListedLabels: req.BlackListedLabels,
|
BlackListedLabels: req.BlackListedLabels,
|
||||||
DisplayExternalContributors: req.DisplayExternalContributors,
|
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)
|
err = handler.SettingsService.StoreSettings(settings)
|
||||||
|
@ -76,8 +128,40 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http
|
||||||
}
|
}
|
||||||
|
|
||||||
type putSettingsRequest struct {
|
type putSettingsRequest struct {
|
||||||
TemplatesURL string `valid:"required"`
|
TemplatesURL string `valid:"required"`
|
||||||
LogoURL string `valid:""`
|
LogoURL string `valid:""`
|
||||||
BlackListedLabels []portainer.Pair `valid:""`
|
BlackListedLabels []portainer.Pair `valid:""`
|
||||||
DisplayExternalContributors bool `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:""`
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
@ -26,11 +25,12 @@ func NewUploadHandler(bouncer *security.RequestBouncer) *UploadHandler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
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)))
|
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUploadTLS)))
|
||||||
return h
|
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) {
|
func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
httperror.WriteMethodNotAllowedResponse(w, []string{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)
|
vars := mux.Vars(r)
|
||||||
endpointID := vars["endpointID"]
|
|
||||||
certificate := vars["certificate"]
|
certificate := vars["certificate"]
|
||||||
ID, err := strconv.Atoi(endpointID)
|
|
||||||
if err != nil {
|
folder := r.FormValue("folder")
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
if folder == "" {
|
||||||
|
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.FileService.StoreTLSFile(portainer.EndpointID(ID), fileType, file)
|
err = handler.FileService.StoreTLSFile(folder, fileType, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
|
|
@ -26,6 +26,7 @@ type UserHandler struct {
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
ResourceControlService portainer.ResourceControlService
|
ResourceControlService portainer.ResourceControlService
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
|
SettingsService portainer.SettingsService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserHandler returns a new instance of UserHandler.
|
// NewUserHandler returns a new instance of UserHandler.
|
||||||
|
@ -93,13 +94,6 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var role portainer.UserRole
|
|
||||||
if req.Role == 1 {
|
|
||||||
role = portainer.AdministratorRole
|
|
||||||
} else {
|
|
||||||
role = portainer.StandardUserRole
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := handler.UserService.UserByUsername(req.Username)
|
user, err := handler.UserService.UserByUsername(req.Username)
|
||||||
if err != nil && err != portainer.ErrUserNotFound {
|
if err != nil && err != portainer.ErrUserNotFound {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
@ -110,16 +104,32 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var role portainer.UserRole
|
||||||
|
if req.Role == 1 {
|
||||||
|
role = portainer.AdministratorRole
|
||||||
|
} else {
|
||||||
|
role = portainer.StandardUserRole
|
||||||
|
}
|
||||||
|
|
||||||
user = &portainer.User{
|
user = &portainer.User{
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Role: role,
|
Role: role,
|
||||||
}
|
}
|
||||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
|
||||||
|
settings, err := handler.SettingsService.Settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
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)
|
err = handler.UserService.CreateUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
@ -135,7 +145,7 @@ type postUsersResponse struct {
|
||||||
|
|
||||||
type postUsersRequest struct {
|
type postUsersRequest struct {
|
||||||
Username string `valid:"required"`
|
Username string `valid:"required"`
|
||||||
Password string `valid:"required"`
|
Password string `valid:""`
|
||||||
Role int `valid:"required"`
|
Role int `valid:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,9 +72,7 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||||
// Should not be managed here
|
// Should not be managed here
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
if endpoint.TLS {
|
if endpoint.TLS {
|
||||||
tlsConfig, err = crypto.CreateTLSConfiguration(endpoint.TLSCACertPath,
|
tlsConfig, err = crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false)
|
||||||
endpoint.TLSCertPath,
|
|
||||||
endpoint.TLSKeyPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -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) {
|
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
u.Scheme = "https"
|
u.Scheme = "https"
|
||||||
proxy := factory.createReverseProxy(u)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ type Server struct {
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
|
LDAPService portainer.LDAPService
|
||||||
Handler *handler.Handler
|
Handler *handler.Handler
|
||||||
SSL bool
|
SSL bool
|
||||||
SSLCert string
|
SSLCert string
|
||||||
|
@ -42,12 +43,15 @@ func (server *Server) Start() error {
|
||||||
authHandler.UserService = server.UserService
|
authHandler.UserService = server.UserService
|
||||||
authHandler.CryptoService = server.CryptoService
|
authHandler.CryptoService = server.CryptoService
|
||||||
authHandler.JWTService = server.JWTService
|
authHandler.JWTService = server.JWTService
|
||||||
|
authHandler.LDAPService = server.LDAPService
|
||||||
|
authHandler.SettingsService = server.SettingsService
|
||||||
var userHandler = handler.NewUserHandler(requestBouncer)
|
var userHandler = handler.NewUserHandler(requestBouncer)
|
||||||
userHandler.UserService = server.UserService
|
userHandler.UserService = server.UserService
|
||||||
userHandler.TeamService = server.TeamService
|
userHandler.TeamService = server.TeamService
|
||||||
userHandler.TeamMembershipService = server.TeamMembershipService
|
userHandler.TeamMembershipService = server.TeamMembershipService
|
||||||
userHandler.CryptoService = server.CryptoService
|
userHandler.CryptoService = server.CryptoService
|
||||||
userHandler.ResourceControlService = server.ResourceControlService
|
userHandler.ResourceControlService = server.ResourceControlService
|
||||||
|
userHandler.SettingsService = server.SettingsService
|
||||||
var teamHandler = handler.NewTeamHandler(requestBouncer)
|
var teamHandler = handler.NewTeamHandler(requestBouncer)
|
||||||
teamHandler.TeamService = server.TeamService
|
teamHandler.TeamService = server.TeamService
|
||||||
teamHandler.TeamMembershipService = server.TeamMembershipService
|
teamHandler.TeamMembershipService = server.TeamMembershipService
|
||||||
|
@ -56,6 +60,8 @@ func (server *Server) Start() error {
|
||||||
var statusHandler = handler.NewStatusHandler(requestBouncer, server.Status)
|
var statusHandler = handler.NewStatusHandler(requestBouncer, server.Status)
|
||||||
var settingsHandler = handler.NewSettingsHandler(requestBouncer)
|
var settingsHandler = handler.NewSettingsHandler(requestBouncer)
|
||||||
settingsHandler.SettingsService = server.SettingsService
|
settingsHandler.SettingsService = server.SettingsService
|
||||||
|
settingsHandler.LDAPService = server.LDAPService
|
||||||
|
settingsHandler.FileService = server.FileService
|
||||||
var templatesHandler = handler.NewTemplatesHandler(requestBouncer)
|
var templatesHandler = handler.NewTemplatesHandler(requestBouncer)
|
||||||
templatesHandler.SettingsService = server.SettingsService
|
templatesHandler.SettingsService = server.SettingsService
|
||||||
var dockerHandler = handler.NewDockerHandler(requestBouncer)
|
var dockerHandler = handler.NewDockerHandler(requestBouncer)
|
||||||
|
|
123
api/ldap/ldap.go
Normal file
123
api/ldap/ldap.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -41,12 +41,40 @@ type (
|
||||||
Version string `json:"Version"`
|
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 represents the application settings.
|
||||||
Settings struct {
|
Settings struct {
|
||||||
TemplatesURL string `json:"TemplatesURL"`
|
TemplatesURL string `json:"TemplatesURL"`
|
||||||
LogoURL string `json:"LogoURL"`
|
LogoURL string `json:"LogoURL"`
|
||||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||||
|
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||||
|
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// User represents a user account.
|
// User represents a user account.
|
||||||
|
@ -64,6 +92,9 @@ type (
|
||||||
// or a regular user
|
// or a regular user
|
||||||
UserRole int
|
UserRole int
|
||||||
|
|
||||||
|
// AuthenticationMethod represents the authentication method used to authenticate a user.
|
||||||
|
AuthenticationMethod int
|
||||||
|
|
||||||
// Team represents a list of user accounts.
|
// Team represents a list of user accounts.
|
||||||
Team struct {
|
Team struct {
|
||||||
ID TeamID `json:"Id"`
|
ID TeamID `json:"Id"`
|
||||||
|
@ -292,22 +323,28 @@ type (
|
||||||
|
|
||||||
// FileService represents a service for managing files.
|
// FileService represents a service for managing files.
|
||||||
FileService interface {
|
FileService interface {
|
||||||
StoreTLSFile(endpointID EndpointID, fileType TLSFileType, r io.Reader) error
|
StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error
|
||||||
GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error)
|
GetPathForTLSFile(folder string, fileType TLSFileType) (string, error)
|
||||||
DeleteTLSFiles(endpointID EndpointID) error
|
DeleteTLSFiles(folder string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointWatcher represents a service to synchronize the endpoints via an external source.
|
// EndpointWatcher represents a service to synchronize the endpoints via an external source.
|
||||||
EndpointWatcher interface {
|
EndpointWatcher interface {
|
||||||
WatchEndpointFile(endpointFilePath string) error
|
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 (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API.
|
// APIVersion is the version number of the Portainer API.
|
||||||
APIVersion = "1.13.6"
|
APIVersion = "1.13.6"
|
||||||
// DBVersion is the version number of the Portainer database.
|
// DBVersion is the version number of the Portainer database.
|
||||||
DBVersion = 2
|
DBVersion = 3
|
||||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||||
)
|
)
|
||||||
|
@ -337,6 +374,14 @@ const (
|
||||||
StandardUserRole
|
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 (
|
const (
|
||||||
_ ResourceAccessLevel = iota
|
_ ResourceAccessLevel = iota
|
||||||
// ReadWriteAccessLevel represents an access level with read-write permissions on a resource
|
// ReadWriteAccessLevel represents an access level with read-write permissions on a resource
|
||||||
|
|
14
app/app.js
14
app/app.js
|
@ -51,6 +51,7 @@ angular.module('portainer', [
|
||||||
'service',
|
'service',
|
||||||
'services',
|
'services',
|
||||||
'settings',
|
'settings',
|
||||||
|
'settingsAuthentication',
|
||||||
'sidebar',
|
'sidebar',
|
||||||
'stats',
|
'stats',
|
||||||
'swarm',
|
'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', {
|
.state('task', {
|
||||||
url: '^/task/:id',
|
url: '^/task/:id',
|
||||||
views: {
|
views: {
|
||||||
|
|
|
@ -98,7 +98,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !port-mapping -->
|
<!-- !port-mapping -->
|
||||||
<!-- access-control -->
|
<!-- access-control -->
|
||||||
<por-access-control-form form-data="formValues.AccessControlData"></por-access-control-form>
|
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||||
<!-- !access-control -->
|
<!-- !access-control -->
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !port-mapping -->
|
<!-- !port-mapping -->
|
||||||
<!-- access-control -->
|
<!-- access-control -->
|
||||||
<por-access-control-form form-data="formValues.AccessControlData"></por-access-control-form>
|
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||||
<!-- !access-control -->
|
<!-- !access-control -->
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !driver-options -->
|
<!-- !driver-options -->
|
||||||
<!-- access-control -->
|
<!-- access-control -->
|
||||||
<por-access-control-form form-data="formValues.AccessControlData"></por-access-control-form>
|
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||||
<!-- !access-control -->
|
<!-- !access-control -->
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Authentication settings">
|
||||||
|
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||||
|
</rd-header-title>
|
||||||
|
<rd-header-content>
|
||||||
|
<a ui-sref="settings">Settings</a> > Authentication
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-users" title="Authentication"></rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Authentication method
|
||||||
|
</div>
|
||||||
|
<div class="form-group"></div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0">
|
||||||
|
<div class="boxselector_wrapper">
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="registry_quay" ng-model="settings.AuthenticationMethod" ng-value="1">
|
||||||
|
<label for="registry_quay">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-users" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Internal
|
||||||
|
</div>
|
||||||
|
<p>Internal authentication mechanism</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="registry_custom" ng-model="settings.AuthenticationMethod" ng-value="2">
|
||||||
|
<label for="registry_custom">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-users" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
LDAP
|
||||||
|
</div>
|
||||||
|
<p>LDAP authentication</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Information
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-if="settings.AuthenticationMethod === 1">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
When using internal authentication, Portainer will encrypt user passwords and store credentials locally.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-if="settings.AuthenticationMethod === 2">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
When using LDAP authentication, Portainer will delegate user authentication to a LDAP server (exception for the <b>admin</b> user that always use internal authentication).
|
||||||
|
<p style="margin-top:5px;">
|
||||||
|
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
<u>Users still need to be created in Portainer beforehand.</u>
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="settings.AuthenticationMethod === 2">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
LDAP configuration
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ldap_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
LDAP URL
|
||||||
|
<portainer-tooltip position="bottom" message="URL or IP address of the LDAP server."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="ldap_url" ng-model="LDAPSettings.URL" placeholder="e.g. 10.0.0.10:389 or myldap.domain.tld:389">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ldap_username" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Reader DN
|
||||||
|
<portainer-tooltip position="bottom" message="Account that will be used to search for users."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="ldap_username" ng-model="LDAPSettings.ReaderDN" placeholder="cn=readonly-account,dc=ldap,dc=domain,dc=tld">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ldap_password" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="password" class="form-control" id="ldap_password" ng-model="LDAPSettings.Password" placeholder="password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="!LDAPSettings.TLSConfig.TLS && !LDAPSettings.StartTLS">
|
||||||
|
<label for="ldap_password" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Connectivity check
|
||||||
|
<i class="fa fa-check green-icon" style="margin-left: 5px;" ng-if="state.successfulConnectivityCheck"></i>
|
||||||
|
<i class="fa fa-times red-icon" style="margin-left: 5px;" ng-if="state.failedConnectivityCheck"></i>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!LDAPSettings.URL || !LDAPSettings.ReaderDN || !LDAPSettings.Password" ng-click="LDAPConnectivityCheck()">Test connectivity</button>
|
||||||
|
<i id="connectivityCheckSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
LDAP security
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- starttls -->
|
||||||
|
<div class="form-group" ng-if="!LDAPSettings.TLSConfig.TLS">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="tls" class="control-label text-left">
|
||||||
|
Use StartTLS
|
||||||
|
<portainer-tooltip position="bottom" message="Enable this option if want to use StartTLS to secure the connection to the server. Ignored if Use TLS is selected."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" ng-model="LDAPSettings.StartTLS"><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !starttls -->
|
||||||
|
|
||||||
|
<!-- tls-checkbox -->
|
||||||
|
<div class="form-group" ng-if="!LDAPSettings.StartTLS">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="tls" class="control-label text-left">
|
||||||
|
Use TLS
|
||||||
|
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the LDAP server."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" ng-model="LDAPSettings.TLSConfig.TLS"><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !tls-checkbox -->
|
||||||
|
|
||||||
|
<!-- tls-skip-verify -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="tls" class="control-label text-left">
|
||||||
|
Skip verification of server certificate
|
||||||
|
<portainer-tooltip position="bottom" message="Skip the verification of the server TLS certificate. Not recommended on unsecured networks."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" ng-model="LDAPSettings.TLSConfig.TLSSkipVerify"><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !tls-skip-verify -->
|
||||||
|
|
||||||
|
<!-- tls-certs -->
|
||||||
|
<div ng-if="LDAPSettings.TLSConfig.TLS || LDAPSettings.StartTLS">
|
||||||
|
<!-- ca-input -->
|
||||||
|
<div class="form-group" ng-if="!LDAPSettings.TLSConfig.TLSSkipVerify">
|
||||||
|
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{ formValues.TLSCACert.name }}
|
||||||
|
<i class="fa fa-check green-icon" ng-if="formValues.TLSCACert && formValues.TLSCACert === LDAPSettings.TLSConfig.TLSCACert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !ca-input -->
|
||||||
|
</div>
|
||||||
|
<!-- !tls-certs -->
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="LDAPSettings.TLSConfig.TLS || LDAPSettings.StartTLS">
|
||||||
|
<label for="ldap_password" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Connectivity check
|
||||||
|
<i class="fa fa-check green-icon" style="margin-left: 5px;" ng-if="state.successfulConnectivityCheck"></i>
|
||||||
|
<i class="fa fa-times red-icon" style="margin-left: 5px;" ng-if="state.failedConnectivityCheck"></i>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" ng-click="LDAPConnectivityCheck()" ng-disabled="!LDAPSettings.URL || !LDAPSettings.ReaderDN || !LDAPSettings.Password || (!formValues.TLSCACert && !LDAPSettings.TLSConfig.TLSSkipVerify)">Test connectivity</button>
|
||||||
|
<i id="connectivityCheckSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
User search configurations
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- search-settings -->
|
||||||
|
<div ng-repeat="config in LDAPSettings.SearchSettings | limitTo: (1 - LDAPSettings.SearchSettings)" style="margin-top: 5px;">
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$index > 0">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
Extra search configuration
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ldap_basedn_{{$index}}" class="col-sm-4 col-md-2 control-label text-left">
|
||||||
|
Base DN
|
||||||
|
<portainer-tooltip position="bottom" message="The distinguished name of the element from which the LDAP server will search for users."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-8 col-md-4">
|
||||||
|
<input type="text" class="form-control" id="ldap_basedn_{{$index}}" ng-model="config.BaseDN" placeholder="dc=ldap,dc=domain,dc=tld">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="ldap_username_att_{{$index}}" class="col-sm-4 col-md-3 col-lg-2 margin-sm-top control-label text-left">
|
||||||
|
Username attribute
|
||||||
|
<portainer-tooltip position="bottom" message="LDAP attribute which denotes the username."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-8 col-md-3 col-lg-4 margin-sm-top">
|
||||||
|
<input type="text" class="form-control" id="ldap_username_att_{{$index}}" ng-model="config.UserNameAttribute" placeholder="uid">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ldap_filter_{{$index}}" class="col-sm-4 col-md-2 control-label text-left">
|
||||||
|
Filter
|
||||||
|
<portainer-tooltip position="bottom" message="The LDAP search filter used to select user elements, optional."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-7 col-md-9">
|
||||||
|
<input type="text" class="form-control" id="ldap_filter_{{$index}}" ng-model="config.Filter" placeholder="(objectClass=account)">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-1" ng-if="$index > 0">
|
||||||
|
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSearchConfiguration($index)">
|
||||||
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSearchConfiguration()">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> add search configuration
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- !search-settings -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- actions -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()">Save</button>
|
||||||
|
<i id="updateSettingsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
<!-- <span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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();
|
||||||
|
}]);
|
|
@ -69,6 +69,9 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||||
<a ui-sref="settings" ui-sref-active="active">Settings <span class="menu-icon fa fa-cogs"></span></a>
|
<a ui-sref="settings" ui-sref-active="active">Settings <span class="menu-icon fa fa-cogs"></span></a>
|
||||||
|
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'settings' || $state.current.name === 'settings_authentication') && applicationState.application.authentication && isAdmin">
|
||||||
|
<a ui-sref="settings_authentication" ui-sref-active="active">Authentication</a>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !env -->
|
<!-- !env -->
|
||||||
<!-- access-control -->
|
<!-- access-control -->
|
||||||
<por-access-control-form form-data="formValues.AccessControlData"></por-access-control-form>
|
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||||
<!-- !access-control -->
|
<!-- !access-control -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
|
@ -32,29 +32,6 @@
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- <tr ng-if="!formValues.Administrator">
|
|
||||||
<td colspan="2">
|
|
||||||
<label for="teams" class="control-label text-left">
|
|
||||||
Teams
|
|
||||||
</label>
|
|
||||||
<span class="small text-muted" style="margin-left: 20px;" ng-if="teams.length === 0">
|
|
||||||
You have not yet created any team. Head over the <a ui-sref="teams">teams view</a> to manage user teams.</span>
|
|
||||||
</span>
|
|
||||||
<span isteven-multi-select
|
|
||||||
ng-if="teams.length > 0"
|
|
||||||
input-model="teams"
|
|
||||||
output-model="formValues.Teams"
|
|
||||||
button-label="Name"
|
|
||||||
item-label="Name"
|
|
||||||
tick-property="ticked"
|
|
||||||
helper-elements="filter"
|
|
||||||
search-property="Name"
|
|
||||||
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
|
|
||||||
style="margin-left: 20px;"
|
|
||||||
on-item-click="onTeamClick(data)"
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr> -->
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
|
@ -62,7 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row" ng-if="AuthenticationMethod === 1">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-lock" title="Change user password"></rd-widget-header>
|
<rd-widget-header icon="fa-lock" title="Change user password"></rd-widget-header>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('user', [])
|
angular.module('user', [])
|
||||||
.controller('UserController', ['$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) {
|
function ($q, $scope, $state, $stateParams, UserService, ModalService, Notifications, SettingsService) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
updatePasswordError: ''
|
updatePasswordError: ''
|
||||||
|
@ -72,12 +72,14 @@ function ($q, $scope, $state, $stateParams, UserService, ModalService, Notificat
|
||||||
function initView() {
|
function initView() {
|
||||||
$('#loadingViewSpinner').show();
|
$('#loadingViewSpinner').show();
|
||||||
$q.all({
|
$q.all({
|
||||||
user: UserService.user($stateParams.id)
|
user: UserService.user($stateParams.id),
|
||||||
|
settings: SettingsService.publicSettings()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var user = data.user;
|
var user = data.user;
|
||||||
$scope.user = user;
|
$scope.user = user;
|
||||||
$scope.formValues.Administrator = user.Role === 1 ? true : false;
|
$scope.formValues.Administrator = user.Role === 1 ? true : false;
|
||||||
|
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve user information');
|
Notifications.error('Failure', err, 'Unable to retrieve user information');
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title="User settings">
|
<rd-header-title title="User settings">
|
||||||
|
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||||
</rd-header-title>
|
</rd-header-title>
|
||||||
<rd-header-content>User settings</rd-header-content>
|
<rd-header-content>User settings</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
@ -58,7 +59,11 @@
|
||||||
<!-- !confirm-password-input -->
|
<!-- !confirm-password-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.currentPassword || formValues.newPassword.length < 8 || formValues.newPassword !== formValues.confirmPassword" ng-click="updatePassword()">Update password</button>
|
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="AuthenticationMethod !== 1 || !formValues.currentPassword || formValues.newPassword.length < 8 || formValues.newPassword !== formValues.confirmPassword" ng-click="updatePassword()">Update password</button>
|
||||||
|
<span class="text-muted small" style="margin-left: 5px;" ng-if="AuthenticationMethod === 2">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
You cannot change your password when using LDAP authentication.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('userSettings', [])
|
angular.module('userSettings', [])
|
||||||
.controller('UserSettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications',
|
.controller('UserSettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications', 'SettingsService',
|
||||||
function ($scope, $state, $sanitize, Authentication, UserService, Notifications) {
|
function ($scope, $state, $sanitize, Authentication, UserService, Notifications, SettingsService) {
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
newPassword: '',
|
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();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<!-- name-input -->
|
<!-- name-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username" class="col-sm-2 control-label text-left">Username</label>
|
<label for="username" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Username
|
||||||
|
<portainer-tooltip ng-if="AuthenticationMethod === 2" position="bottom" message="Username must exactly match username defined in external LDAP source."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" id="username" ng-model="formValues.Username" ng-change="checkUsernameValidity()" placeholder="e.g. jdoe">
|
<input type="text" class="form-control" id="username" ng-model="formValues.Username" ng-change="checkUsernameValidity()" placeholder="e.g. jdoe">
|
||||||
|
@ -27,8 +30,8 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
<!-- new-password-input -->
|
<!-- new-password-input -->
|
||||||
<div class="form-group">
|
<div class="form-group" ng-if="AuthenticationMethod === 1">
|
||||||
<label for="password" class="col-sm-2 control-label text-left">Password</label>
|
<label for="password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||||
|
@ -38,8 +41,8 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !new-password-input -->
|
<!-- !new-password-input -->
|
||||||
<!-- confirm-password-input -->
|
<!-- confirm-password-input -->
|
||||||
<div class="form-group">
|
<div class="form-group" ng-if="AuthenticationMethod === 1">
|
||||||
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
|
<label for="confirm_password" class="col-sm-3 col-lg-2 control-label text-left">Confirm password</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||||
|
@ -95,7 +98,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!state.validUsername || formValues.Username === '' || formValues.Password === '' || formValues.Password !== formValues.ConfirmPassword" ng-click="addUser()"><i class="fa fa-user-plus" aria-hidden="true"></i> Add user</button>
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!state.validUsername || formValues.Username === '' || (AuthenticationMethod === 1 && formValues.Password === '') || (AuthenticationMethod === 1 && formValues.Password !== formValues.ConfirmPassword)" ng-click="addUser()"><i class="fa fa-user-plus" aria-hidden="true"></i> Add user</button>
|
||||||
<i id="createUserSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
<i id="createUserSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
<span class="text-danger" ng-if="state.userCreationError" style="margin: 5px;">
|
<span class="text-danger" ng-if="state.userCreationError" style="margin: 5px;">
|
||||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.userCreationError }}
|
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.userCreationError }}
|
||||||
|
@ -140,19 +143,26 @@
|
||||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="users" ng-click="order('Username')">
|
<a ng-click="order('Username')">
|
||||||
Name
|
Name
|
||||||
<span ng-show="sortType == 'Username' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'Username' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'Username' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Username' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="users" ng-click="order('RoleName')">
|
<a ng-click="order('RoleName')">
|
||||||
Role
|
Role
|
||||||
<span ng-show="sortType == 'RoleName' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'RoleName' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'RoleName' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'RoleName' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="order('AuthenticationMethod')">
|
||||||
|
Authentication
|
||||||
|
<span ng-show="sortType == 'AuthenticationMethod' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'AuthenticationMethod' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th ng-if="isAdmin"></th>
|
<th ng-if="isAdmin"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -166,6 +176,10 @@
|
||||||
<i ng-if="user.isTeamLeader" class="fa fa-user-plus" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i ng-if="user.isTeamLeader" class="fa fa-user-plus" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
{{ user.RoleName }}
|
{{ user.RoleName }}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span ng-if="AuthenticationMethod === 1 || user.Id === 1">Internal</span>
|
||||||
|
<span ng-if="AuthenticationMethod === 2 && user.Id !== 1">LDAP</span>
|
||||||
|
</td>
|
||||||
<td ng-if="isAdmin">
|
<td ng-if="isAdmin">
|
||||||
<a ui-sref="user({id: user.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
|
<a ui-sref="user({id: user.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('users', [])
|
angular.module('users', [])
|
||||||
.controller('UsersController', ['$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) {
|
function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication, SettingsService) {
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
userCreationError: '',
|
userCreationError: '',
|
||||||
selectedItemCount: 0,
|
selectedItemCount: 0,
|
||||||
|
@ -140,13 +140,15 @@ function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershi
|
||||||
$q.all({
|
$q.all({
|
||||||
users: UserService.users(true),
|
users: UserService.users(true),
|
||||||
teams: isAdmin ? TeamService.teams() : UserService.userLeadingTeams(userDetails.ID),
|
teams: isAdmin ? TeamService.teams() : UserService.userLeadingTeams(userDetails.ID),
|
||||||
memberships: TeamMembershipService.memberships()
|
memberships: TeamMembershipService.memberships(),
|
||||||
|
settings: SettingsService.publicSettings()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var users = data.users;
|
var users = data.users;
|
||||||
assignTeamLeaders(users, data.memberships);
|
assignTeamLeaders(users, data.memberships);
|
||||||
$scope.users = users;
|
$scope.users = users;
|
||||||
$scope.teams = data.teams;
|
$scope.teams = data.teams;
|
||||||
|
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve users and teams');
|
Notifications.error('Failure', err, 'Unable to retrieve users and teams');
|
||||||
|
|
12
app/models/api/settings/ldapSettings.js
Normal file
12
app/models/api/settings/ldapSettings.js
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -3,4 +3,6 @@ function SettingsViewModel(data) {
|
||||||
this.LogoURL = data.LogoURL;
|
this.LogoURL = data.LogoURL;
|
||||||
this.BlackListedLabels = data.BlackListedLabels;
|
this.BlackListedLabels = data.BlackListedLabels;
|
||||||
this.DisplayExternalContributors = data.DisplayExternalContributors;
|
this.DisplayExternalContributors = data.DisplayExternalContributors;
|
||||||
|
this.AuthenticationMethod = data.AuthenticationMethod;
|
||||||
|
this.LDAPSettings = data.LDAPSettings;
|
||||||
}
|
}
|
|
@ -7,5 +7,6 @@ function UserViewModel(data) {
|
||||||
} else {
|
} else {
|
||||||
this.RoleName = 'user';
|
this.RoleName = 'user';
|
||||||
}
|
}
|
||||||
|
this.AuthenticationMethod = data.AuthenticationMethod;
|
||||||
this.Checked = false;
|
this.Checked = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
angular.module('portainer.rest')
|
angular.module('portainer.rest')
|
||||||
.factory('Settings', ['$resource', 'API_ENDPOINT_SETTINGS', function SettingsFactory($resource, API_ENDPOINT_SETTINGS) {
|
.factory('Settings', ['$resource', 'API_ENDPOINT_SETTINGS', function SettingsFactory($resource, API_ENDPOINT_SETTINGS) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource(API_ENDPOINT_SETTINGS, {}, {
|
return $resource(API_ENDPOINT_SETTINGS + '/:subResource/:action', {}, {
|
||||||
get: { method: 'GET' },
|
get: { method: 'GET' },
|
||||||
update: { method: 'PUT' }
|
update: { method: 'PUT' },
|
||||||
|
publicSettings: { method: 'GET', params: { subResource: 'public' } },
|
||||||
|
checkLDAPConnectivity: { method: 'PUT', params: { subResource: 'authentication', action: 'checkLDAP' } }
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -22,5 +22,24 @@ angular.module('portainer.services')
|
||||||
return Settings.update({}, settings).$promise;
|
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;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,44 +1,44 @@
|
||||||
angular.module('portainer.services')
|
angular.module('portainer.services')
|
||||||
.factory('FileUploadService', ['$q', 'Upload', function FileUploadFactory($q, Upload) {
|
.factory('FileUploadService', ['$q', 'Upload', function FileUploadFactory($q, Upload) {
|
||||||
'use strict';
|
'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 service = {};
|
||||||
var uploadTLSCA = uploadFile('api/upload/tls/' + endpointID + '/ca', TLSCAFile);
|
|
||||||
queue.push(uploadTLSCA);
|
function uploadFile(url, file) {
|
||||||
}
|
return Upload.upload({ url: url, data: { file: file }});
|
||||||
if (TLSCertFile) {
|
}
|
||||||
var uploadTLSCert = uploadFile('api/upload/tls/' + endpointID + '/cert', TLSCertFile);
|
|
||||||
queue.push(uploadTLSCert);
|
service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
}
|
var queue = [];
|
||||||
if (TLSKeyFile) {
|
|
||||||
var uploadTLSKey = uploadFile('api/upload/tls/' + endpointID + '/key', TLSKeyFile);
|
if (TLSCAFile) {
|
||||||
queue.push(uploadTLSKey);
|
queue.push(uploadFile('api/upload/tls/ca?folder=ldap', TLSCAFile));
|
||||||
}
|
|
||||||
$q.all(queue).then(function (data) {
|
|
||||||
deferred.resolve(data);
|
|
||||||
}, function (err) {
|
|
||||||
deferred.reject(err);
|
|
||||||
}, function update(evt) {
|
|
||||||
deferred.notify(evt);
|
|
||||||
});
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -44,7 +44,7 @@ angular.module('portainer.services')
|
||||||
deferred.resolve(state);
|
deferred.resolve(state);
|
||||||
} else {
|
} else {
|
||||||
$q.all({
|
$q.all({
|
||||||
settings: SettingsService.settings(),
|
settings: SettingsService.publicSettings(),
|
||||||
status: StatusService.status()
|
status: StatusService.status()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
|
|
|
@ -82,10 +82,6 @@ a[ng-click]{
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa.green-icon {
|
|
||||||
color: #23ae89;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.portainer-tooltip .tooltip-inner {
|
.tooltip.portainer-tooltip .tooltip-inner {
|
||||||
font-family: Montserrat;
|
font-family: Montserrat;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
@ -106,6 +102,10 @@ a[ng-click]{
|
||||||
color: #337ab7;
|
color: #337ab7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fa.green-icon {
|
||||||
|
color: #23ae89;
|
||||||
|
}
|
||||||
|
|
||||||
.fa.red-icon {
|
.fa.red-icon {
|
||||||
color: #ae2323;
|
color: #ae2323;
|
||||||
}
|
}
|
||||||
|
@ -517,4 +517,4 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
.monospaced {
|
.monospaced {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue