mirror of https://github.com/portainer/portainer
feat(authentication): add LDAP authentication support (#1093)
parent
04ea81e7cd
commit
d27528a771
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:""`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
14
app/app.js
14
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: {
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- 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 -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- 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 -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
</div>
|
||||
<!-- !driver-options -->
|
||||
<!-- 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 -->
|
||||
<!-- actions -->
|
||||
<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 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>
|
||||
<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>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
</div>
|
||||
<!-- !env -->
|
||||
<!-- 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 -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
|
|
@ -32,29 +32,6 @@
|
|||
</label>
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
|
@ -62,7 +39,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" ng-if="AuthenticationMethod === 1">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-lock" title="Change user password"></rd-widget-header>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('user', [])
|
||||
.controller('UserController', ['$q', '$scope', '$state', '$stateParams', 'UserService', 'ModalService', 'Notifications',
|
||||
function ($q, $scope, $state, $stateParams, UserService, ModalService, Notifications) {
|
||||
.controller('UserController', ['$q', '$scope', '$state', '$stateParams', 'UserService', 'ModalService', 'Notifications', 'SettingsService',
|
||||
function ($q, $scope, $state, $stateParams, UserService, ModalService, Notifications, SettingsService) {
|
||||
|
||||
$scope.state = {
|
||||
updatePasswordError: ''
|
||||
|
@ -72,12 +72,14 @@ function ($q, $scope, $state, $stateParams, UserService, ModalService, Notificat
|
|||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all({
|
||||
user: UserService.user($stateParams.id)
|
||||
user: UserService.user($stateParams.id),
|
||||
settings: SettingsService.publicSettings()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var user = data.user;
|
||||
$scope.user = user;
|
||||
$scope.formValues.Administrator = user.Role === 1 ? true : false;
|
||||
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve user information');
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<rd-header>
|
||||
<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-content>User settings</rd-header-content>
|
||||
</rd-header>
|
||||
|
@ -58,7 +59,11 @@
|
|||
<!-- !confirm-password-input -->
|
||||
<div class="form-group">
|
||||
<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>
|
||||
</form>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('userSettings', [])
|
||||
.controller('UserSettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications',
|
||||
function ($scope, $state, $sanitize, Authentication, UserService, Notifications) {
|
||||
.controller('UserSettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications', 'SettingsService',
|
||||
function ($scope, $state, $sanitize, Authentication, UserService, Notifications, SettingsService) {
|
||||
$scope.formValues = {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
|
@ -26,4 +26,19 @@ function ($scope, $state, $sanitize, Authentication, UserService, Notifications)
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
SettingsService.publicSettings()
|
||||
.then(function success(data) {
|
||||
$scope.AuthenticationMethod = data.AuthenticationMethod;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
|
|
@ -17,7 +17,10 @@
|
|||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<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="input-group">
|
||||
<input type="text" class="form-control" id="username" ng-model="formValues.Username" ng-change="checkUsernameValidity()" placeholder="e.g. jdoe">
|
||||
|
@ -27,8 +30,8 @@
|
|||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- new-password-input -->
|
||||
<div class="form-group">
|
||||
<label for="password" class="col-sm-2 control-label text-left">Password</label>
|
||||
<div class="form-group" ng-if="AuthenticationMethod === 1">
|
||||
<label for="password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
|
@ -38,8 +41,8 @@
|
|||
</div>
|
||||
<!-- !new-password-input -->
|
||||
<!-- confirm-password-input -->
|
||||
<div class="form-group">
|
||||
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
|
||||
<div class="form-group" ng-if="AuthenticationMethod === 1">
|
||||
<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="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
|
@ -95,7 +98,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<span class="text-danger" ng-if="state.userCreationError" style="margin: 5px;">
|
||||
<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)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="users" ng-click="order('Username')">
|
||||
<a ng-click="order('Username')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Username' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Username' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="users" ng-click="order('RoleName')">
|
||||
<a ng-click="order('RoleName')">
|
||||
Role
|
||||
<span ng-show="sortType == 'RoleName' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'RoleName' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -166,6 +176,10 @@
|
|||
<i ng-if="user.isTeamLeader" class="fa fa-user-plus" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ user.RoleName }}
|
||||
</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">
|
||||
<a ui-sref="user({id: user.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
|
||||
</td>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('users', [])
|
||||
.controller('UsersController', ['$q', '$scope', '$state', '$sanitize', 'UserService', 'TeamService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication',
|
||||
function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication) {
|
||||
.controller('UsersController', ['$q', '$scope', '$state', '$sanitize', 'UserService', 'TeamService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication', 'SettingsService',
|
||||
function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication, SettingsService) {
|
||||
$scope.state = {
|
||||
userCreationError: '',
|
||||
selectedItemCount: 0,
|
||||
|
@ -140,13 +140,15 @@ function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershi
|
|||
$q.all({
|
||||
users: UserService.users(true),
|
||||
teams: isAdmin ? TeamService.teams() : UserService.userLeadingTeams(userDetails.ID),
|
||||
memberships: TeamMembershipService.memberships()
|
||||
memberships: TeamMembershipService.memberships(),
|
||||
settings: SettingsService.publicSettings()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var users = data.users;
|
||||
assignTeamLeaders(users, data.memberships);
|
||||
$scope.users = users;
|
||||
$scope.teams = data.teams;
|
||||
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve users and teams');
|
||||
|
|
|
@ -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.BlackListedLabels = data.BlackListedLabels;
|
||||
this.DisplayExternalContributors = data.DisplayExternalContributors;
|
||||
this.AuthenticationMethod = data.AuthenticationMethod;
|
||||
this.LDAPSettings = data.LDAPSettings;
|
||||
}
|
|
@ -7,5 +7,6 @@ function UserViewModel(data) {
|
|||
} else {
|
||||
this.RoleName = 'user';
|
||||
}
|
||||
this.AuthenticationMethod = data.AuthenticationMethod;
|
||||
this.Checked = false;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
angular.module('portainer.rest')
|
||||
.factory('Settings', ['$resource', 'API_ENDPOINT_SETTINGS', function SettingsFactory($resource, API_ENDPOINT_SETTINGS) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_SETTINGS, {}, {
|
||||
return $resource(API_ENDPOINT_SETTINGS + '/:subResource/:action', {}, {
|
||||
get: { method: 'GET' },
|
||||
update: { method: 'PUT' }
|
||||
update: { method: 'PUT' },
|
||||
publicSettings: { method: 'GET', params: { subResource: 'public' } },
|
||||
checkLDAPConnectivity: { method: 'PUT', params: { subResource: 'authentication', action: 'checkLDAP' } }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -22,5 +22,24 @@ angular.module('portainer.services')
|
|||
return Settings.update({}, settings).$promise;
|
||||
};
|
||||
|
||||
service.publicSettings = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Settings.publicSettings().$promise
|
||||
.then(function success(data) {
|
||||
var settings = new SettingsViewModel(data);
|
||||
deferred.resolve(settings);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve application settings', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.checkLDAPConnectivity = function(settings) {
|
||||
return Settings.checkLDAPConnectivity({}, settings).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -1,44 +1,44 @@
|
|||
angular.module('portainer.services')
|
||||
.factory('FileUploadService', ['$q', 'Upload', function FileUploadFactory($q, Upload) {
|
||||
'use strict';
|
||||
function uploadFile(url, file) {
|
||||
var deferred = $q.defer();
|
||||
Upload.upload({
|
||||
url: url,
|
||||
data: { file: file }
|
||||
}).then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
}, function error(e) {
|
||||
deferred.reject(e);
|
||||
}, function progress(evt) {
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
return {
|
||||
uploadTLSFilesForEndpoint: function(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
var deferred = $q.defer();
|
||||
var queue = [];
|
||||
|
||||
if (TLSCAFile) {
|
||||
var uploadTLSCA = uploadFile('api/upload/tls/' + endpointID + '/ca', TLSCAFile);
|
||||
queue.push(uploadTLSCA);
|
||||
}
|
||||
if (TLSCertFile) {
|
||||
var uploadTLSCert = uploadFile('api/upload/tls/' + endpointID + '/cert', TLSCertFile);
|
||||
queue.push(uploadTLSCert);
|
||||
}
|
||||
if (TLSKeyFile) {
|
||||
var uploadTLSKey = uploadFile('api/upload/tls/' + endpointID + '/key', TLSKeyFile);
|
||||
queue.push(uploadTLSKey);
|
||||
}
|
||||
$q.all(queue).then(function (data) {
|
||||
deferred.resolve(data);
|
||||
}, function (err) {
|
||||
deferred.reject(err);
|
||||
}, function update(evt) {
|
||||
deferred.notify(evt);
|
||||
});
|
||||
return deferred.promise;
|
||||
var service = {};
|
||||
|
||||
function uploadFile(url, file) {
|
||||
return Upload.upload({ url: url, data: { file: file }});
|
||||
}
|
||||
|
||||
service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
var queue = [];
|
||||
|
||||
if (TLSCAFile) {
|
||||
queue.push(uploadFile('api/upload/tls/ca?folder=ldap', TLSCAFile));
|
||||
}
|
||||
if (TLSCertFile) {
|
||||
queue.push(uploadFile('api/upload/tls/cert?folder=ldap', TLSCertFile));
|
||||
}
|
||||
if (TLSKeyFile) {
|
||||
queue.push(uploadFile('api/upload/tls/key?folder=ldap', TLSKeyFile));
|
||||
}
|
||||
|
||||
return $q.all(queue);
|
||||
};
|
||||
|
||||
service.uploadTLSFilesForEndpoint = function(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
var queue = [];
|
||||
|
||||
if (TLSCAFile) {
|
||||
queue.push(uploadFile('api/upload/tls/ca?folder=' + endpointID, TLSCAFile));
|
||||
}
|
||||
if (TLSCertFile) {
|
||||
queue.push(uploadFile('api/upload/tls/cert?folder=' + endpointID, TLSCertFile));
|
||||
}
|
||||
if (TLSKeyFile) {
|
||||
queue.push(uploadFile('api/upload/tls/key?folder=' + endpointID, TLSKeyFile));
|
||||
}
|
||||
|
||||
return $q.all(queue);
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -44,7 +44,7 @@ angular.module('portainer.services')
|
|||
deferred.resolve(state);
|
||||
} else {
|
||||
$q.all({
|
||||
settings: SettingsService.settings(),
|
||||
settings: SettingsService.publicSettings(),
|
||||
status: StatusService.status()
|
||||
})
|
||||
.then(function success(data) {
|
||||
|
|
|
@ -82,10 +82,6 @@ a[ng-click]{
|
|||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.fa.green-icon {
|
||||
color: #23ae89;
|
||||
}
|
||||
|
||||
.tooltip.portainer-tooltip .tooltip-inner {
|
||||
font-family: Montserrat;
|
||||
background-color: #ffffff;
|
||||
|
@ -106,6 +102,10 @@ a[ng-click]{
|
|||
color: #337ab7;
|
||||
}
|
||||
|
||||
.fa.green-icon {
|
||||
color: #23ae89;
|
||||
}
|
||||
|
||||
.fa.red-icon {
|
||||
color: #ae2323;
|
||||
}
|
||||
|
@ -517,4 +517,4 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
|||
.monospaced {
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue