From 8d4807c9e7dae17d51607c73c4a299062915e513 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 14 Sep 2017 08:08:37 +0200 Subject: [PATCH] feat(api): TLS endpoint creation and init overhaul (#1173) --- api/bolt/migrate_dbversion2.go | 2 +- api/bolt/migrate_dbversion3.go | 27 ++ api/bolt/migrator.go | 16 +- api/cmd/portainer/main.go | 15 +- api/cron/endpoint_sync.go | 67 ++- api/crypto/tls.go | 45 +- api/file/file.go | 25 +- api/http/handler/endpoint.go | 89 ++-- api/http/handler/websocket.go | 4 +- api/http/proxy/factory.go | 2 +- api/http/proxy/manager.go | 2 +- api/ldap/ldap.go | 2 +- api/portainer.go | 49 ++- api/swagger.yaml | 400 +++++++++++------- app/components/endpoint/endpoint.html | 73 +--- app/components/endpoint/endpointController.js | 60 +-- app/components/endpoints/endpoints.html | 80 +--- .../endpoints/endpointsController.js | 25 +- .../initAdmin/initAdminController.js | 17 +- app/components/initEndpoint/initEndpoint.html | 89 ++-- .../initEndpoint/initEndpointController.js | 33 +- .../endpointSecurity/por-endpoint-security.js | 12 + .../endpointSecurity/porEndpointSecurity.html | 126 ++++++ .../porEndpointSecurityController.js | 32 ++ .../porEndpointSecurityModel.js | 7 + app/services/api/endpointService.js | 10 +- 26 files changed, 828 insertions(+), 481 deletions(-) create mode 100644 api/bolt/migrate_dbversion3.go create mode 100644 app/directives/endpointSecurity/por-endpoint-security.js create mode 100644 app/directives/endpointSecurity/porEndpointSecurity.html create mode 100644 app/directives/endpointSecurity/porEndpointSecurityController.js create mode 100644 app/directives/endpointSecurity/porEndpointSecurityModel.js diff --git a/api/bolt/migrate_dbversion2.go b/api/bolt/migrate_dbversion2.go index 86059736d..38a3e4b50 100644 --- a/api/bolt/migrate_dbversion2.go +++ b/api/bolt/migrate_dbversion2.go @@ -2,7 +2,7 @@ package bolt import "github.com/portainer/portainer" -func (m *Migrator) updateSettingsToVersion3() error { +func (m *Migrator) updateSettingsToDBVersion3() error { legacySettings, err := m.SettingsService.Settings() if err != nil { return err diff --git a/api/bolt/migrate_dbversion3.go b/api/bolt/migrate_dbversion3.go new file mode 100644 index 000000000..d8679ca68 --- /dev/null +++ b/api/bolt/migrate_dbversion3.go @@ -0,0 +1,27 @@ +package bolt + +import "github.com/portainer/portainer" + +func (m *Migrator) updateEndpointsToDBVersion4() error { + legacyEndpoints, err := m.EndpointService.Endpoints() + if err != nil { + return err + } + + for _, endpoint := range legacyEndpoints { + endpoint.TLSConfig = portainer.TLSConfiguration{} + if endpoint.TLS { + endpoint.TLSConfig.TLS = true + endpoint.TLSConfig.TLSSkipVerify = false + endpoint.TLSConfig.TLSCACertPath = endpoint.TLSCACertPath + endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath + endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath + } + err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator.go b/api/bolt/migrator.go index 297afb867..faba56157 100644 --- a/api/bolt/migrator.go +++ b/api/bolt/migrator.go @@ -30,7 +30,7 @@ func NewMigrator(store *Store, version int) *Migrator { func (m *Migrator) Migrate() error { // Portainer < 1.12 - if m.CurrentDBVersion == 0 { + if m.CurrentDBVersion < 1 { err := m.updateAdminUserToDBVersion1() if err != nil { return err @@ -38,7 +38,7 @@ func (m *Migrator) Migrate() error { } // Portainer 1.12.x - if m.CurrentDBVersion == 1 { + if m.CurrentDBVersion < 2 { err := m.updateResourceControlsToDBVersion2() if err != nil { return err @@ -50,8 +50,16 @@ func (m *Migrator) Migrate() error { } // Portainer 1.13.x - if m.CurrentDBVersion == 2 { - err := m.updateSettingsToVersion3() + if m.CurrentDBVersion < 3 { + err := m.updateSettingsToDBVersion3() + if err != nil { + return err + } + } + + // Portainer 1.14.0 + if m.CurrentDBVersion < 4 { + err := m.updateEndpointsToDBVersion4() if err != nil { return err } diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index bb9839907..6f202ec32 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -191,12 +191,15 @@ func main() { } if len(endpoints) == 0 { endpoint := &portainer.Endpoint{ - Name: "primary", - URL: *flags.Endpoint, - TLS: *flags.TLSVerify, - TLSCACertPath: *flags.TLSCacert, - TLSCertPath: *flags.TLSCert, - TLSKeyPath: *flags.TLSKey, + Name: "primary", + URL: *flags.Endpoint, + TLSConfig: portainer.TLSConfiguration{ + TLS: *flags.TLSVerify, + TLSSkipVerify: false, + TLSCACertPath: *flags.TLSCacert, + TLSCertPath: *flags.TLSCert, + TLSKeyPath: *flags.TLSKey, + }, AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, } diff --git a/api/cron/endpoint_sync.go b/api/cron/endpoint_sync.go index 9fbb634dc..6b9926665 100644 --- a/api/cron/endpoint_sync.go +++ b/api/cron/endpoint_sync.go @@ -22,6 +22,16 @@ type ( endpointsToUpdate []*portainer.Endpoint endpointsToDelete []*portainer.Endpoint } + + fileEndpoint struct { + Name string `json:"Name"` + URL string `json:"URL"` + TLS bool `json:"TLS,omitempty"` + TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"` + TLSCACert string `json:"TLSCACert,omitempty"` + TLSCert string `json:"TLSCert,omitempty"` + TLSKey string `json:"TLSKey,omitempty"` + } ) const ( @@ -55,6 +65,28 @@ func isValidEndpoint(endpoint *portainer.Endpoint) bool { return false } +func convertFileEndpoints(fileEndpoints []fileEndpoint) []portainer.Endpoint { + convertedEndpoints := make([]portainer.Endpoint, 0) + + for _, e := range fileEndpoints { + endpoint := portainer.Endpoint{ + Name: e.Name, + URL: e.URL, + TLSConfig: portainer.TLSConfiguration{}, + } + if e.TLS { + endpoint.TLSConfig.TLS = true + endpoint.TLSConfig.TLSSkipVerify = e.TLSSkipVerify + endpoint.TLSConfig.TLSCACertPath = e.TLSCACert + endpoint.TLSConfig.TLSCertPath = e.TLSCert + endpoint.TLSConfig.TLSKeyPath = e.TLSKey + } + convertedEndpoints = append(convertedEndpoints, endpoint) + } + + return convertedEndpoints +} + func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int { for idx, v := range endpoints { if endpoint.Name == v.Name && isValidEndpoint(&v) { @@ -66,22 +98,25 @@ func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint { var endpoint *portainer.Endpoint - if original.URL != updated.URL || original.TLS != updated.TLS || - (updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) || - (updated.TLS && original.TLSCertPath != updated.TLSCertPath) || - (updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) { + if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS || + (updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) || + (updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) || + (updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) || + (updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) { endpoint = original endpoint.URL = updated.URL - if updated.TLS { - endpoint.TLS = true - endpoint.TLSCACertPath = updated.TLSCACertPath - endpoint.TLSCertPath = updated.TLSCertPath - endpoint.TLSKeyPath = updated.TLSKeyPath + if updated.TLSConfig.TLS { + endpoint.TLSConfig.TLS = true + endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify + endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath + endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath + endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath } else { - endpoint.TLS = false - endpoint.TLSCACertPath = "" - endpoint.TLSCertPath = "" - endpoint.TLSKeyPath = "" + endpoint.TLSConfig.TLS = false + endpoint.TLSConfig.TLSSkipVerify = false + endpoint.TLSConfig.TLSCACertPath = "" + endpoint.TLSConfig.TLSCertPath = "" + endpoint.TLSConfig.TLSKeyPath = "" } } return endpoint @@ -141,7 +176,7 @@ func (job endpointSyncJob) Sync() error { return err } - var fileEndpoints []portainer.Endpoint + var fileEndpoints []fileEndpoint err = json.Unmarshal(data, &fileEndpoints) if endpointSyncError(err, job.logger) { return err @@ -156,7 +191,9 @@ func (job endpointSyncJob) Sync() error { return err } - sync := job.prepareSyncData(storedEndpoints, fileEndpoints) + convertedFileEndpoints := convertFileEndpoints(fileEndpoints) + + sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints) if sync.requireSync() { err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete) if endpointSyncError(err, job.logger) { diff --git a/api/crypto/tls.go b/api/crypto/tls.go index 9c3f1f192..3d22091d8 100644 --- a/api/crypto/tls.go +++ b/api/crypto/tls.go @@ -4,31 +4,38 @@ import ( "crypto/tls" "crypto/x509" "io/ioutil" + + "github.com/portainer/portainer" ) // CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key -func CreateTLSConfiguration(caCertPath, certPath, keyPath string, skipTLSVerify bool) (*tls.Config, error) { +func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) { + TLSConfig := &tls.Config{} - config := &tls.Config{} + if config.TLS { + if config.TLSCertPath != "" && config.TLSKeyPath != "" { + cert, err := tls.LoadX509KeyPair(config.TLSCertPath, config.TLSKeyPath) + if err != nil { + return nil, err + } - if certPath != "" && keyPath != "" { - cert, err := tls.LoadX509KeyPair(certPath, keyPath) - if err != nil { - return nil, err + TLSConfig.Certificates = []tls.Certificate{cert} } - config.Certificates = []tls.Certificate{cert} + + if !config.TLSSkipVerify { + caCert, err := ioutil.ReadFile(config.TLSCACertPath) + if err != nil { + return nil, err + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + TLSConfig.RootCAs = caCertPool + } + + TLSConfig.InsecureSkipVerify = config.TLSSkipVerify } - 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 + return TLSConfig, nil } diff --git a/api/file/file.go b/api/file/file.go index 75bcb99ec..c143fce0b 100644 --- a/api/file/file.go +++ b/api/file/file.go @@ -95,7 +95,7 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil } -// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint. +// DeleteTLSFiles deletes a folder in the TLS store path. func (service *Service) DeleteTLSFiles(folder string) error { storePath := path.Join(service.fileStorePath, TLSStorePath, folder) err := os.RemoveAll(storePath) @@ -105,6 +105,29 @@ func (service *Service) DeleteTLSFiles(folder string) error { return nil } +// DeleteTLSFile deletes a specific TLS file from a folder. +func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileType) error { + var fileName string + switch fileType { + case portainer.TLSFileCA: + fileName = TLSCACertFile + case portainer.TLSFileCert: + fileName = TLSCertFile + case portainer.TLSFileKey: + fileName = TLSKeyFile + default: + return portainer.ErrUndefinedTLSFileType + } + + filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName) + + err := os.Remove(filePath) + if err != nil { + return err + } + return nil +} + // createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system. func (service *Service) createDirectoryInStoreIfNotExist(name string) error { path := path.Join(service.fileStorePath, name) diff --git a/api/http/handler/endpoint.go b/api/http/handler/endpoint.go index 07950d790..1b052847e 100644 --- a/api/http/handler/endpoint.go +++ b/api/http/handler/endpoint.go @@ -57,10 +57,12 @@ func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManag type ( postEndpointsRequest struct { - Name string `valid:"required"` - URL string `valid:"required"` - PublicURL string `valid:"-"` - TLS bool + Name string `valid:"required"` + URL string `valid:"required"` + PublicURL string `valid:"-"` + TLS bool + TLSSkipVerify bool + TLSSkipClientVerify bool } postEndpointsResponse struct { @@ -73,10 +75,12 @@ type ( } putEndpointsRequest struct { - Name string `valid:"-"` - URL string `valid:"-"` - PublicURL string `valid:"-"` - TLS bool `valid:"-"` + Name string `valid:"-"` + URL string `valid:"-"` + PublicURL string `valid:"-"` + TLS bool `valid:"-"` + TLSSkipVerify bool `valid:"-"` + TLSSkipClientVerify bool `valid:"-"` } ) @@ -123,10 +127,13 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht } endpoint := &portainer.Endpoint{ - Name: req.Name, - URL: req.URL, - PublicURL: req.PublicURL, - TLS: req.TLS, + Name: req.Name, + URL: req.URL, + PublicURL: req.PublicURL, + TLSConfig: portainer.TLSConfiguration{ + TLS: req.TLS, + TLSSkipVerify: req.TLSSkipVerify, + }, AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, } @@ -139,12 +146,19 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht if req.TLS { folder := strconv.Itoa(int(endpoint.ID)) - caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) - endpoint.TLSCACertPath = caCertPath - certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) - endpoint.TLSCertPath = certPath - keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) - endpoint.TLSKeyPath = keyPath + + if !req.TLSSkipVerify { + caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) + endpoint.TLSConfig.TLSCACertPath = caCertPath + } + + if !req.TLSSkipClientVerify { + certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) + endpoint.TLSConfig.TLSCertPath = certPath + keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) + endpoint.TLSConfig.TLSKeyPath = keyPath + } + err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) @@ -284,18 +298,33 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http folder := strconv.Itoa(int(endpoint.ID)) if req.TLS { - endpoint.TLS = true - caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) - endpoint.TLSCACertPath = caCertPath - certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) - endpoint.TLSCertPath = certPath - keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) - endpoint.TLSKeyPath = keyPath + endpoint.TLSConfig.TLS = true + endpoint.TLSConfig.TLSSkipVerify = req.TLSSkipVerify + if !req.TLSSkipVerify { + caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) + endpoint.TLSConfig.TLSCACertPath = caCertPath + } else { + endpoint.TLSConfig.TLSCACertPath = "" + handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA) + } + + if !req.TLSSkipClientVerify { + certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) + endpoint.TLSConfig.TLSCertPath = certPath + keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) + endpoint.TLSConfig.TLSKeyPath = keyPath + } else { + endpoint.TLSConfig.TLSCertPath = "" + handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert) + endpoint.TLSConfig.TLSKeyPath = "" + handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey) + } } else { - endpoint.TLS = false - endpoint.TLSCACertPath = "" - endpoint.TLSCertPath = "" - endpoint.TLSKeyPath = "" + endpoint.TLSConfig.TLS = false + endpoint.TLSConfig.TLSSkipVerify = true + endpoint.TLSConfig.TLSCACertPath = "" + endpoint.TLSConfig.TLSCertPath = "" + endpoint.TLSConfig.TLSKeyPath = "" err = handler.FileService.DeleteTLSFiles(folder) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) @@ -350,7 +379,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h return } - if endpoint.TLS { + if endpoint.TLSConfig.TLS { err = handler.FileService.DeleteTLSFiles(id) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) diff --git a/api/http/handler/websocket.go b/api/http/handler/websocket.go index dbc4fd9f0..b57f02806 100644 --- a/api/http/handler/websocket.go +++ b/api/http/handler/websocket.go @@ -71,8 +71,8 @@ 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, false) + if endpoint.TLSConfig.TLS { + tlsConfig, err = crypto.CreateTLSConfiguration(&endpoint.TLSConfig) if err != nil { log.Fatalf("Unable to create TLS configuration: %s", err) return diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go index dc733149f..210ef54a0 100644 --- a/api/http/proxy/factory.go +++ b/api/http/proxy/factory.go @@ -24,7 +24,7 @@ func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler { func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) { u.Scheme = "https" proxy := factory.createReverseProxy(u) - config, err := crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false) + config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig) if err != nil { return nil, err } diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 8710c7a44..bdba2b216 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -37,7 +37,7 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht } if endpointURL.Scheme == "tcp" { - if endpoint.TLS { + if endpoint.TLSConfig.TLS { proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint) if err != nil { return nil, err diff --git a/api/ldap/ldap.go b/api/ldap/ldap.go index 786332c35..8a280f754 100644 --- a/api/ldap/ldap.go +++ b/api/ldap/ldap.go @@ -52,7 +52,7 @@ func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearc func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) { if settings.TLSConfig.TLS || settings.StartTLS { - config, err := crypto.CreateTLSConfiguration(settings.TLSConfig.TLSCACertPath, "", "", settings.TLSConfig.TLSSkipVerify) + config, err := crypto.CreateTLSConfiguration(&settings.TLSConfig) if err != nil { return nil, err } diff --git a/api/portainer.go b/api/portainer.go index 69d8e9290..13d8b82e1 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -155,16 +155,20 @@ type ( // Endpoint represents a Docker endpoint with all the info required // to connect to it. Endpoint struct { - ID EndpointID `json:"Id"` - Name string `json:"Name"` - URL string `json:"URL"` - PublicURL string `json:"PublicURL"` - TLS bool `json:"TLS"` - TLSCACertPath string `json:"TLSCACert,omitempty"` - TLSCertPath string `json:"TLSCert,omitempty"` - TLSKeyPath string `json:"TLSKey,omitempty"` - AuthorizedUsers []UserID `json:"AuthorizedUsers"` - AuthorizedTeams []TeamID `json:"AuthorizedTeams"` + ID EndpointID `json:"Id"` + Name string `json:"Name"` + URL string `json:"URL"` + PublicURL string `json:"PublicURL"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + AuthorizedUsers []UserID `json:"AuthorizedUsers"` + AuthorizedTeams []TeamID `json:"AuthorizedTeams"` + + // Deprecated fields + // Deprecated in DBVersion == 4 + TLS bool `json:"TLS,omitempty"` + TLSCACertPath string `json:"TLSCACert,omitempty"` + TLSCertPath string `json:"TLSCert,omitempty"` + TLSKeyPath string `json:"TLSKey,omitempty"` } // ResourceControlID represents a resource control identifier. @@ -172,20 +176,18 @@ type ( // ResourceControl represent a reference to a Docker resource with specific access controls ResourceControl struct { - ID ResourceControlID `json:"Id"` - ResourceID string `json:"ResourceId"` - SubResourceIDs []string `json:"SubResourceIds"` - Type ResourceControlType `json:"Type"` - AdministratorsOnly bool `json:"AdministratorsOnly"` - - UserAccesses []UserResourceAccess `json:"UserAccesses"` - TeamAccesses []TeamResourceAccess `json:"TeamAccesses"` + ID ResourceControlID `json:"Id"` + ResourceID string `json:"ResourceId"` + SubResourceIDs []string `json:"SubResourceIds"` + Type ResourceControlType `json:"Type"` + AdministratorsOnly bool `json:"AdministratorsOnly"` + UserAccesses []UserResourceAccess `json:"UserAccesses"` + TeamAccesses []TeamResourceAccess `json:"TeamAccesses"` // Deprecated fields - // Deprecated: OwnerID field is deprecated in DBVersion == 2 - OwnerID UserID `json:"OwnerId"` - // Deprecated: AccessLevel field is deprecated in DBVersion == 2 - AccessLevel ResourceAccessLevel `json:"AccessLevel"` + // Deprecated in DBVersion == 2 + OwnerID UserID `json:"OwnerId,omitempty"` + AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"` } // ResourceControlType represents the type of resource associated to the resource control (volume, container, service). @@ -325,6 +327,7 @@ type ( FileService interface { StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error GetPathForTLSFile(folder string, fileType TLSFileType) (string, error) + DeleteTLSFile(folder string, fileType TLSFileType) error DeleteTLSFiles(folder string) error } @@ -344,7 +347,7 @@ const ( // APIVersion is the version number of the Portainer API. APIVersion = "1.14.0" // DBVersion is the version number of the Portainer database. - DBVersion = 3 + DBVersion = 4 // DefaultTemplatesURL represents the default URL for the templates definitions. DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json" ) diff --git a/api/swagger.yaml b/api/swagger.yaml index 21f4dac79..59ac0bdce 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1,38 +1,61 @@ --- swagger: "2.0" info: - description: "Portainer API is an HTTP API served by Portainer. It is used by the\ - \ Portainer UI and everything you can do with the UI can be done using the HTTP\ - \ API. \nExamples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8\ - \ \nYou can find out more about Portainer at [http://portainer.io](http://portainer.io)\ - \ and get some support on [Slack](http://portainer.io/slack/).\n\n# Authentication\n\ - \nMost of the API endpoints require to be authenticated as well as some level\ - \ of authorization to be used.\nPortainer API uses JSON Web Token to manage authentication\ - \ and thus requires you to provide a token in the **Authorization** header of\ - \ each request\nwith the **Bearer** authentication mechanism.\n\nExample:\n```\n\ - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE\n\ - ```\n\n# Security\n\nEach API endpoint has an associated access policy, it is\ - \ documented in the description of each endpoint.\n\nDifferent access policies\ - \ are available:\n* Public access\n* Authenticated access\n* Restricted access\n\ - * Administrator access\n\n### Public access\n\nNo authentication is required to\ - \ access the endpoints with this access policy.\n\n### Authenticated access\n\n\ - Authentication is required to access the endpoints with this access policy.\n\n\ - ### Restricted access\n\nAuthentication is required to access the endpoints with\ - \ this access policy.\nExtra-checks might be added to ensure access to the resource\ - \ is granted. Returned data might also be filtered.\n\n### Administrator access\n\ - \nAuthentication as well as an administrator role are required to access the endpoints\ - \ with this access policy.\n\n# Execute Docker requests\n\nPortainer **DO NOT**\ - \ expose specific endpoints to manage your Docker resources (create a container,\ - \ remove a volume, etc...). \n\nInstead, it acts as a reverse-proxy to the Docker\ - \ HTTP API. This means that you can execute Docker requests **via** the Portainer\ - \ HTTP API.\n\nTo do so, you can use the `/endpoints/{id}/docker` Portainer API\ - \ endpoint (which is not documented below due to Swagger limitations). This\n\ - endpoint has a restricted access policy so you still need to be authenticated\ - \ to be able to query this endpoint. Any query on this endpoint will be proxied\ - \ to the\nDocker API of the associated endpoint (requests and responses objects\ - \ are the same as documented in the Docker API).\n\n**NOTE**: You can find more\ - \ information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/)\ - \ as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).\n" + description: | + Portainer API is an HTTP API served by Portainer. It is used by the Portainer UI and everything you can do with the UI can be done using the HTTP API. + Examples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8 + You can find out more about Portainer at [http://portainer.io](http://portainer.io) and get some support on [Slack](http://portainer.io/slack/). + + # Authentication + + Most of the API endpoints require to be authenticated as well as some level of authorization to be used. + Portainer API uses JSON Web Token to manage authentication and thus requires you to provide a token in the **Authorization** header of each request + with the **Bearer** authentication mechanism. + + Example: + ``` + Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE + ``` + + # Security + + Each API endpoint has an associated access policy, it is documented in the description of each endpoint. + + Different access policies are available: + * Public access + * Authenticated access + * Restricted access + * Administrator access + + ### Public access + + No authentication is required to access the endpoints with this access policy. + + ### Authenticated access + + Authentication is required to access the endpoints with this access policy. + + ### Restricted access + + Authentication is required to access the endpoints with this access policy. + Extra-checks might be added to ensure access to the resource is granted. Returned data might also be filtered. + + ### Administrator access + + Authentication as well as an administrator role are required to access the endpoints with this access policy. + + # Execute Docker requests + + Portainer **DO NOT** expose specific endpoints to manage your Docker resources (create a container, remove a volume, etc...). + + Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you can execute Docker requests **via** the Portainer HTTP API. + + To do so, you can use the `/endpoints/{id}/docker` Portainer API endpoint (which is not documented below due to Swagger limitations). This + endpoint has a restricted access policy so you still need to be authenticated to be able to query this endpoint. Any query on this endpoint will be proxied to the + Docker API of the associated endpoint (requests and responses objects are the same as documented in the Docker API). + + **NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). + version: "1.14.0" title: "Portainer API" contact: @@ -75,8 +98,9 @@ paths: tags: - "auth" summary: "Authenticate a user" - description: "Use this endpoint to authenticate against Portainer using a username\ - \ and password. \n**Access policy**: public\n" + description: | + Use this endpoint to authenticate against Portainer using a username and password. + **Access policy**: public operationId: "AuthenticateUser" consumes: - "application/json" @@ -117,8 +141,9 @@ paths: tags: - "dockerhub" summary: "Retrieve DockerHub information" - description: "Use this endpoint to retrieve the information used to connect\ - \ to the DockerHub \n**Access policy**: authenticated\n" + description: | + Use this endpoint to retrieve the information used to connect to the DockerHub + **Access policy**: authenticated operationId: "DockerHubInspect" produces: - "application/json" @@ -136,8 +161,9 @@ paths: tags: - "dockerhub" summary: "Update DockerHub information" - description: "Use this endpoint to update the information used to connect to\ - \ the DockerHub \n**Access policy**: administrator\n" + description: | + Use this endpoint to update the information used to connect to the DockerHub + **Access policy**: administrator operationId: "DockerHubUpdate" consumes: - "application/json" @@ -169,9 +195,11 @@ paths: tags: - "endpoints" summary: "List endpoints" - description: "List all endpoints based on the current user authorizations. Will\n\ - return all endpoints if using an administrator account otherwise it will\n\ - only return authorized endpoints. \n**Access policy**: restricted \n" + description: | + List all endpoints based on the current user authorizations. Will + return all endpoints if using an administrator account otherwise it will + only return authorized endpoints. + **Access policy**: restricted operationId: "EndpointList" produces: - "application/json" @@ -189,8 +217,9 @@ paths: tags: - "endpoints" summary: "Create a new endpoint" - description: "Create a new endpoint that will be used to manage a Docker environment.\ - \ \n**Access policy**: administrator\n" + description: | + Create a new endpoint that will be used to manage a Docker environment. + **Access policy**: administrator operationId: "EndpointCreate" consumes: - "application/json" @@ -231,8 +260,9 @@ paths: tags: - "endpoints" summary: "Inspect an endpoint" - description: "Retrieve details abount an endpoint. \n**Access policy**: administrator\ - \ \n" + description: | + Retrieve details abount an endpoint. + **Access policy**: administrator operationId: "EndpointInspect" produces: - "application/json" @@ -269,7 +299,9 @@ paths: tags: - "endpoints" summary: "Update an endpoint" - description: "Update an endpoint. \n**Access policy**: administrator\n" + description: | + Update an endpoint. + **Access policy**: administrator operationId: "EndpointUpdate" consumes: - "application/json" @@ -319,7 +351,9 @@ paths: tags: - "endpoints" summary: "Remove an endpoint" - description: "Remove an endpoint. \n**Access policy**: administrator \n" + description: | + Remove an endpoint. + **Access policy**: administrator operationId: "EndpointDelete" parameters: - name: "id" @@ -360,8 +394,9 @@ paths: tags: - "endpoints" summary: "Manage accesses to an endpoint" - description: "Manage user and team accesses to an endpoint. \n**Access policy**:\ - \ administrator \n" + description: | + Manage user and team accesses to an endpoint. + **Access policy**: administrator operationId: "EndpointAccessUpdate" consumes: - "application/json" @@ -400,15 +435,17 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" + /registries: get: tags: - "registries" summary: "List registries" - description: "List all registries based on the current user authorizations.\n\ - Will return all registries if using an administrator account otherwise it\n\ - will only return authorized registries. \n**Access policy**: restricted \ - \ \n" + description: | + List all registries based on the current user authorizations. + Will return all registries if using an administrator account otherwise it + will only return authorized registries. + **Access policy**: restricted operationId: "RegistryList" produces: - "application/json" @@ -426,8 +463,9 @@ paths: tags: - "registries" summary: "Create a new registry" - description: "Create a new registry. \n**Access policy**: administrator \ - \ \n" + description: | + Create a new registry. + **Access policy**: administrator operationId: "RegistryCreate" consumes: - "application/json" @@ -468,8 +506,9 @@ paths: tags: - "registries" summary: "Inspect a registry" - description: "Retrieve details about a registry. \n**Access policy**: administrator\ - \ \n" + description: | + Retrieve details about a registry. + **Access policy**: administrator operationId: "RegistryInspect" produces: - "application/json" @@ -506,7 +545,9 @@ paths: tags: - "registries" summary: "Update a registry" - description: "Update a registry. \n**Access policy**: administrator \n" + description: | + Update a registry. + **Access policy**: administrator operationId: "RegistryUpdate" consumes: - "application/json" @@ -563,8 +604,9 @@ paths: tags: - "registries" summary: "Remove a registry" - description: "Remove a registry. \n**Access policy**: administrator \ - \ \n" + description: | + Remove a registry. + **Access policy**: administrator operationId: "RegistryDelete" parameters: - name: "id" @@ -598,8 +640,9 @@ paths: tags: - "registries" summary: "Manage accesses to a registry" - description: "Manage user and team accesses to a registry. \n**Access policy**:\ - \ administrator \n" + description: | + Manage user and team accesses to a registry. + **Access policy**: administrator operationId: "RegistryAccessUpdate" consumes: - "application/json" @@ -643,8 +686,9 @@ paths: tags: - "resource_controls" summary: "Create a new resource control" - description: "Create a new resource control to restrict access to a Docker resource.\ - \ \n**Access policy**: restricted \n" + description: | + Create a new resource control to restrict access to a Docker resource. + **Access policy**: restricted operationId: "ResourceControlCreate" consumes: - "application/json" @@ -690,8 +734,9 @@ paths: tags: - "resource_controls" summary: "Update a resource control" - description: "Update a resource control. \n**Access policy**: restricted \ - \ \n" + description: | + Update a resource control. + **Access policy**: restricted operationId: "ResourceControlUpdate" consumes: - "application/json" @@ -741,8 +786,9 @@ paths: tags: - "resource_controls" summary: "Remove a resource control" - description: "Remove a resource control. \n**Access policy**: restricted \ - \ \n" + description: | + Remove a resource control. + **Access policy**: restricted operationId: "ResourceControlDelete" parameters: - name: "id" @@ -783,8 +829,9 @@ paths: tags: - "settings" summary: "Retrieve Portainer settings" - description: "Retrieve Portainer settings. \n**Access policy**: administrator\ - \ \n" + description: | + Retrieve Portainer settings. + **Access policy**: administrator operationId: "SettingsInspect" produces: - "application/json" @@ -802,8 +849,9 @@ paths: tags: - "settings" summary: "Update Portainer settings" - description: "Update Portainer settings. \n**Access policy**: administrator\ - \ \n" + description: | + Update Portainer settings. + **Access policy**: administrator operationId: "SettingsUpdate" consumes: - "application/json" @@ -835,9 +883,9 @@ paths: tags: - "settings" summary: "Retrieve Portainer public settings" - description: "Retrieve public settings. Returns a small set of settings that\ - \ are not reserved to administrators only. \n**Access policy**: public \ - \ \n" + description: | + Retrieve public settings. Returns a small set of settings that are not reserved to administrators only. + **Access policy**: public operationId: "PublicSettingsInspect" produces: - "application/json" @@ -856,8 +904,9 @@ paths: tags: - "settings" summary: "Test LDAP connectivity" - description: "Test LDAP connectivity using LDAP details. \n**Access policy**:\ - \ administrator \n" + description: | + Test LDAP connectivity using LDAP details. + **Access policy**: administrator operationId: "SettingsLDAPCheck" consumes: - "application/json" @@ -889,8 +938,9 @@ paths: tags: - "status" summary: "Check Portainer status" - description: "Retrieve Portainer status. \n**Access policy**: public \ - \ \n" + description: | + Retrieve Portainer status. + **Access policy**: public operationId: "StatusInspect" produces: - "application/json" @@ -909,9 +959,9 @@ paths: tags: - "users" summary: "List users" - description: "List Portainer users. Non-administrator users will only be able\ - \ to list other non-administrator user accounts. \n**Access policy**: restricted\ - \ \n" + description: | + List Portainer users. Non-administrator users will only be able to list other non-administrator user accounts. + **Access policy**: restricted operationId: "UserList" produces: - "application/json" @@ -929,9 +979,10 @@ paths: tags: - "users" summary: "Create a new user" - description: "Create a new Portainer user. Only team leaders and administrators\ - \ can create users. Only administrators can\ncreate an administrator user\ - \ account. \n**Access policy**: restricted \n" + description: | + Create a new Portainer user. Only team leaders and administrators can create users. Only administrators can + create an administrator user account. + **Access policy**: restricted operationId: "UserCreate" consumes: - "application/json" @@ -979,8 +1030,9 @@ paths: tags: - "users" summary: "Inspect a user" - description: "Retrieve details about a user. \n**Access policy**: administrator\ - \ \n" + description: | + Retrieve details about a user. + **Access policy**: administrator operationId: "UserInspect" produces: - "application/json" @@ -1017,8 +1069,9 @@ paths: tags: - "users" summary: "Update a user" - description: "Update user details. A regular user account can only update his\ - \ details. \n**Access policy**: authenticated \n" + description: | + Update user details. A regular user account can only update his details. + **Access policy**: authenticated operationId: "UserUpdate" consumes: - "application/json" @@ -1068,7 +1121,9 @@ paths: tags: - "users" summary: "Remove a user" - description: "Remove a user. \n**Access policy**: administrator \n" + description: | + Remove a user. + **Access policy**: administrator operationId: "UserDelete" parameters: - name: "id" @@ -1102,8 +1157,9 @@ paths: tags: - "users" summary: "Inspect a user memberships" - description: "Inspect a user memberships. \n**Access policy**: authenticated\ - \ \n" + description: | + Inspect a user memberships. + **Access policy**: authenticated operationId: "UserMembershipsInspect" produces: - "application/json" @@ -1136,13 +1192,15 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" + /users/{id}/passwd: post: tags: - "users" summary: "Check password validity for a user" - description: "Check if the submitted password is valid for the specified user.\ - \ \n**Access policy**: authenticated \n" + description: | + Check if the submitted password is valid for the specified user. + **Access policy**: authenticated operationId: "UserPasswordCheck" consumes: - "application/json" @@ -1183,13 +1241,15 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" + /users/admin/check: get: tags: - "users" summary: "Check administrator account existence" - description: "Check if an administrator account exists in the database.\n**Access\ - \ policy**: public \n" + description: | + Check if an administrator account exists in the database. + **Access policy**: public operationId: "UserAdminCheck" produces: - "application/json" @@ -1210,13 +1270,15 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" + /users/admin/init: post: tags: - "users" summary: "Initialize administrator account" - description: "Initialize the 'admin' user account.\n**Access policy**: public\ - \ \n" + description: | + Initialize the 'admin' user account. + **Access policy**: public operationId: "UserAdminInit" consumes: - "application/json" @@ -1250,34 +1312,35 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" + /upload/tls/{certificate}: post: tags: - "upload" summary: "Upload TLS files" - description: "Use this endpoint to upload TLS files. \n**Access policy**: administrator\n" + description: | + Use this endpoint to upload TLS files. + **Access policy**: administrator operationId: "UploadTLS" consumes: - - "multipart/form-data" + - multipart/form-data produces: - "application/json" parameters: - - name: "certificate" - in: "path" + - in: "path" + name: "certificate" description: "TLS file type. Valid values are 'ca', 'cert' or 'key'." required: true type: "string" - - name: "folder" - in: "query" - description: "Folder where the TLS file will be stored. Will be created if\ - \ not existing." + - in: "query" + name: "folder" + description: "Folder where the TLS file will be stored. Will be created if not existing." required: true type: "string" - - name: "file" - in: "formData" - description: "The file to upload." - required: false + - in: "formData" + name: "file" type: "file" + description: "The file to upload." responses: 200: description: "Success" @@ -1292,13 +1355,15 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" + /teams: get: tags: - "teams" summary: "List teams" - description: "List teams. For non-administrator users, will only list the teams\ - \ they are member of. \n**Access policy**: restricted \n" + description: | + List teams. For non-administrator users, will only list the teams they are member of. + **Access policy**: restricted operationId: "TeamList" produces: - "application/json" @@ -1316,8 +1381,9 @@ paths: tags: - "teams" summary: "Create a new team" - description: "Create a new team. \n**Access policy**: administrator \ - \ \n" + description: | + Create a new team. + **Access policy**: administrator operationId: "TeamCreate" consumes: - "application/json" @@ -1365,8 +1431,9 @@ paths: tags: - "teams" summary: "Inspect a team" - description: "Retrieve details about a team. Access is only available for administrator\ - \ and leaders of that team. \n**Access policy**: restricted \n" + description: | + Retrieve details about a team. Access is only available for administrator and leaders of that team. + **Access policy**: restricted operationId: "TeamInspect" produces: - "application/json" @@ -1410,8 +1477,9 @@ paths: tags: - "teams" summary: "Update a team" - description: "Update a team. \n**Access policy**: administrator \ - \ \n" + description: | + Update a team. + **Access policy**: administrator operationId: "TeamUpdate" consumes: - "application/json" @@ -1454,7 +1522,9 @@ paths: tags: - "teams" summary: "Remove a team" - description: "Remove a team. \n**Access policy**: administrator \n" + description: | + Remove a team. + **Access policy**: administrator operationId: "TeamDelete" parameters: - name: "id" @@ -1483,13 +1553,15 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" + /teams/{id}/memberships: get: tags: - "teams" summary: "Inspect a team memberships" - description: "Inspect a team memberships. Access is only available for administrator\ - \ and leaders of that team. \n**Access policy**: restricted \n" + description: | + Inspect a team memberships. Access is only available for administrator and leaders of that team. + **Access policy**: restricted operationId: "TeamMembershipsInspect" produces: - "application/json" @@ -1522,13 +1594,15 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" + /team_memberships: get: tags: - "team_memberships" summary: "List team memberships" - description: "List team memberships. Access is only available to administrators\ - \ and team leaders. \n**Access policy**: restricted \n" + description: | + List team memberships. Access is only available to administrators and team leaders. + **Access policy**: restricted operationId: "TeamMembershipList" produces: - "application/json" @@ -1553,8 +1627,9 @@ paths: tags: - "team_memberships" summary: "Create a new team membership" - description: "Create a new team memberships. Access is only available to administrators\ - \ leaders of the associated team. \n**Access policy**: restricted \n" + description: | + Create a new team memberships. Access is only available to administrators leaders of the associated team. + **Access policy**: restricted operationId: "TeamMembershipCreate" consumes: - "application/json" @@ -1602,9 +1677,9 @@ paths: tags: - "team_memberships" summary: "Update a team membership" - description: "Update a team membership. Access is only available to administrators\ - \ leaders of the associated team. \n**Access policy**: restricted \ - \ \n" + description: | + Update a team membership. Access is only available to administrators leaders of the associated team. + **Access policy**: restricted operationId: "TeamMembershipUpdate" consumes: - "application/json" @@ -1654,8 +1729,9 @@ paths: tags: - "team_memberships" summary: "Remove a team membership" - description: "Remove a team membership. Access is only available to administrators\ - \ leaders of the associated team. \n**Access policy**: restricted \n" + description: | + Remove a team membership. Access is only available to administrators leaders of the associated team. + **Access policy**: restricted operationId: "TeamMembershipDelete" parameters: - name: "id" @@ -1696,17 +1772,18 @@ paths: tags: - "templates" summary: "Retrieve App templates" - description: "Retrieve App templates. \nYou can find more information about\ - \ the format at http://portainer.readthedocs.io/en/stable/templates.html \ - \ \n**Access policy**: authenticated \n" + description: | + Retrieve App templates. + You can find more information about the format at http://portainer.readthedocs.io/en/stable/templates.html + **Access policy**: authenticated operationId: "TemplateList" produces: - "application/json" parameters: - name: "key" in: "query" - description: "Templates key. Valid values are 'container' or 'linuxserver.io'." required: true + description: "Templates key. Valid values are 'container' or 'linuxserver.io'." type: "string" responses: 200: @@ -1811,8 +1888,8 @@ definitions: AuthenticationMethod: type: "integer" example: 1 - description: "Active authentication method for the Portainer instance. Valid\ - \ values are: 1 for managed or 2 for LDAP." + description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP." + TLSConfiguration: type: "object" properties: @@ -1836,14 +1913,14 @@ definitions: type: "string" example: "/data/tls/key.pem" description: "Path to the TLS client key file" + LDAPSearchSettings: type: "object" properties: BaseDN: type: "string" example: "dc=ldap,dc=domain,dc=tld" - description: "The distinguished name of the element from which the LDAP server\ - \ will search for users" + description: "The distinguished name of the element from which the LDAP server will search for users" Filter: type: "string" example: "(objectClass=account)" @@ -1852,6 +1929,7 @@ definitions: type: "string" example: "uid" description: "LDAP attribute which denotes the username" + LDAPSettings: type: "object" properties: @@ -1877,6 +1955,7 @@ definitions: type: "array" items: $ref: "#/definitions/LDAPSearchSettings" + Settings: type: "object" properties: @@ -1905,8 +1984,7 @@ definitions: AuthenticationMethod: type: "integer" example: 1 - description: "Active authentication method for the Portainer instance. Valid\ - \ values are: 1 for managed or 2 for LDAP." + description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP." LDAPSettings: $ref: "#/definitions/LDAPSettings" Settings_BlackListedLabels: @@ -2072,6 +2150,14 @@ definitions: type: "boolean" example: true description: "Require TLS to connect against this endpoint" + TLSSkipVerify: + type: "boolean" + example: false + description: "Skip server verification when using TLS" + TLSSkipClientVerify: + type: "boolean" + example: false + description: "Skip client verification when using TLS" EndpointCreateResponse: type: "object" properties: @@ -2103,6 +2189,14 @@ definitions: type: "boolean" example: true description: "Require TLS to connect against this endpoint" + TLSSkipVerify: + type: "boolean" + example: false + description: "Skip server verification when using TLS" + TLSSkipClientVerify: + type: "boolean" + example: false + description: "Skip client verification when using TLS" EndpointAccessUpdateRequest: type: "object" properties: @@ -2269,8 +2363,8 @@ definitions: SettingsUpdateRequest: type: "object" required: - - "AuthenticationMethod" - "TemplatesURL" + - "AuthenticationMethod" properties: TemplatesURL: type: "string" @@ -2297,8 +2391,7 @@ definitions: AuthenticationMethod: type: "integer" example: 1 - description: "Active authentication method for the Portainer instance. Valid\ - \ values are: 1 for managed or 2 for LDAP." + description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP." LDAPSettings: $ref: "#/definitions/LDAPSettings" UserCreateRequest: @@ -2395,12 +2488,13 @@ definitions: type: "array" items: $ref: "#/definitions/TeamMembership" + TeamMembershipCreateRequest: type: "object" required: - - "Role" - - "TeamID" - "UserID" + - "TeamID" + - "Role" properties: UserID: type: "integer" @@ -2413,8 +2507,7 @@ definitions: Role: type: "integer" example: 1 - description: "Role for the user inside the team (1 for leader and 2 for regular\ - \ member)" + description: "Role for the user inside the team (1 for leader and 2 for regular member)" TeamMembershipCreateResponse: type: "object" properties: @@ -2429,9 +2522,9 @@ definitions: TeamMembershipUpdateRequest: type: "object" required: - - "Role" - - "TeamID" - "UserID" + - "TeamID" + - "Role" properties: UserID: type: "integer" @@ -2444,8 +2537,7 @@ definitions: Role: type: "integer" example: 1 - description: "Role for the user inside the team (1 for leader and 2 for regular\ - \ member)" + description: "Role for the user inside the team (1 for leader and 2 for regular member)" SettingsLDAPCheckRequest: type: "object" properties: @@ -2454,14 +2546,10 @@ definitions: UserAdminInitRequest: type: "object" properties: - Username: - type: "string" - example: "admin" - description: "Username of the initial administrator account" Password: type: "string" example: "admin-password" - description: "Password of the initial administrator account" + description: "Password for the admin user" TemplateListResponse: type: "array" items: diff --git a/app/components/endpoint/endpoint.html b/app/components/endpoint/endpoint.html index 80be7c92c..f7a113ab1 100644 --- a/app/components/endpoint/endpoint.html +++ b/app/components/endpoint/endpoint.html @@ -12,6 +12,9 @@
+
+ Configuration +
@@ -42,73 +45,19 @@
- -
-
- - + +
+
+ Security
+
- - -
- -
- -
- - - {{ formValues.TLSCACert.name }} - - - - -
-
- - -
- -
- - - {{ formValues.TLSCert.name }} - - - - -
-
- - -
- -
- - - {{ formValues.TLSKey.name }} - - - - -
-
- -
- +
- + Cancel - - - {{ state.error }} - +
diff --git a/app/components/endpoint/endpointController.js b/app/components/endpoint/endpointController.js index 81444370e..bb3803f93 100644 --- a/app/components/endpoint/endpointController.js +++ b/app/components/endpoint/endpointController.js @@ -7,35 +7,41 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications) } $scope.state = { - error: '', uploadInProgress: false }; $scope.formValues = { - TLSCACert: null, - TLSCert: null, - TLSKey: null + SecurityFormData: new EndpointSecurityFormData() }; $scope.updateEndpoint = function() { - var ID = $scope.endpoint.Id; + var endpoint = $scope.endpoint; + var securityData = $scope.formValues.SecurityFormData; + var TLS = securityData.TLS; + var TLSMode = securityData.TLSMode; + var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only'); + var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only'); + var endpointParams = { - name: $scope.endpoint.Name, - URL: $scope.endpoint.URL, - PublicURL: $scope.endpoint.PublicURL, - TLS: $scope.endpoint.TLS, - TLSCACert: $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null, - TLSCert: $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null, - TLSKey: $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null, + name: endpoint.Name, + URL: endpoint.URL, + PublicURL: endpoint.PublicURL, + TLS: TLS, + TLSSkipVerify: TLSSkipVerify, + TLSSkipClientVerify: TLSSkipClientVerify, + TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert, + TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert, + TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey, type: $scope.endpointType }; - EndpointService.updateEndpoint(ID, endpointParams) + $('updateResourceSpinner').show(); + EndpointService.updateEndpoint(endpoint.Id, endpointParams) .then(function success(data) { Notifications.success('Endpoint updated', $scope.endpoint.Name); $state.go('endpoints'); }, function error(err) { - $scope.state.error = err.msg; + Notifications.error('Failure', err, 'Unable to update endpoint'); }, function update(evt) { if (evt.upload) { $scope.state.uploadInProgress = evt.upload; @@ -43,25 +49,27 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications) }); }; - function getEndpoint(endpointID) { + function initView() { $('#loadingViewSpinner').show(); - EndpointService.endpoint($stateParams.id).then(function success(data) { - $('#loadingViewSpinner').hide(); - $scope.endpoint = data; - if (data.URL.indexOf('unix://') === 0) { + EndpointService.endpoint($stateParams.id) + .then(function success(data) { + var endpoint = data; + endpoint.URL = $filter('stripprotocol')(endpoint.URL); + $scope.endpoint = endpoint; + + if (endpoint.URL.indexOf('unix://') === 0) { $scope.endpointType = 'local'; } else { $scope.endpointType = 'remote'; } - $scope.endpoint.URL = $filter('stripprotocol')(data.URL); - $scope.formValues.TLSCACert = data.TLSCACert; - $scope.formValues.TLSCert = data.TLSCert; - $scope.formValues.TLSKey = data.TLSKey; - }, function error(err) { - $('#loadingViewSpinner').hide(); + }) + .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve endpoint details'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); }); } - getEndpoint($stateParams.id); + initView(); }]); diff --git a/app/components/endpoints/endpoints.html b/app/components/endpoints/endpoints.html index 6528ae711..4812eabe8 100644 --- a/app/components/endpoints/endpoints.html +++ b/app/components/endpoints/endpoints.html @@ -60,75 +60,21 @@
- + + + +
- - -
+ +
- - -
- -
- -
- - - {{ formValues.TLSCACert.name }} - - - -
-
- - -
- -
- - - {{ formValues.TLSCert.name }} - - - -
-
- - -
- -
- - - {{ formValues.TLSKey.name }} - - - -
-
- -
- -
-
- - - - {{ state.error }} - -
-
- -
-
- + + + + + +
@@ -191,7 +137,7 @@ Manage access - + Loading... diff --git a/app/components/endpoints/endpointsController.js b/app/components/endpoints/endpointsController.js index c7b488b3c..9a761a164 100644 --- a/app/components/endpoints/endpointsController.js +++ b/app/components/endpoints/endpointsController.js @@ -2,7 +2,6 @@ angular.module('endpoints', []) .controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Notifications', 'Pagination', function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagination) { $scope.state = { - error: '', uploadInProgress: false, selectedItemCount: 0, pagination_count: Pagination.getPaginationCount('endpoints') @@ -14,10 +13,7 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi Name: '', URL: '', PublicURL: '', - TLS: false, - TLSCACert: null, - TLSCert: null, - TLSKey: null + SecurityFormData: new EndpointSecurityFormData() }; $scope.order = function(sortType) { @@ -47,23 +43,28 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi }; $scope.addEndpoint = function() { - $scope.state.error = ''; var name = $scope.formValues.Name; var URL = $scope.formValues.URL; var PublicURL = $scope.formValues.PublicURL; if (PublicURL === '') { PublicURL = URL.split(':')[0]; } - var TLS = $scope.formValues.TLS; - var TLSCAFile = $scope.formValues.TLSCACert; - var TLSCertFile = $scope.formValues.TLSCert; - var TLSKeyFile = $scope.formValues.TLSKey; - EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, false).then(function success(data) { + + var securityData = $scope.formValues.SecurityFormData; + var TLS = securityData.TLS; + var TLSMode = securityData.TLSMode; + var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only'); + var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only'); + var TLSCAFile = TLSSkipVerify ? null : securityData.TLSCACert; + var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert; + var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey; + + EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile).then(function success(data) { Notifications.success('Endpoint created', name); $state.reload(); }, function error(err) { $scope.state.uploadInProgress = false; - $scope.state.error = err.msg; + Notifications.error('Failure', err, 'Unable to create endpoint'); }, function update(evt) { if (evt.upload) { $scope.state.uploadInProgress = evt.upload; diff --git a/app/components/initAdmin/initAdminController.js b/app/components/initAdmin/initAdminController.js index a396cbd17..0959fcd2e 100644 --- a/app/components/initAdmin/initAdminController.js +++ b/app/components/initAdmin/initAdminController.js @@ -23,19 +23,18 @@ function ($scope, $state, $sanitize, Notifications, Authentication, StateManager return EndpointService.endpoints(); }) .then(function success(data) { - var endpoints = data; - if (endpoints.length > 0) { - var endpoint = endpoints[0]; - EndpointProvider.setEndpointID(endpoint.Id); - StateManager.updateEndpointState(true) - .then(function success(data) { + if (data.length === 0) { + $state.go('init.endpoint'); + } else { + var endpointID = data[0].Id; + EndpointProvider.setEndpointID(endpointID); + StateManager.updateEndpointState(false) + .then(function success() { $state.go('dashboard'); }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint'); + Notifications.error('Failure', err, 'Unable to connect to Docker environment'); }); - } else { - $state.go('init.endpoint'); } }) .catch(function error(err) { diff --git a/app/components/initEndpoint/initEndpoint.html b/app/components/initEndpoint/initEndpoint.html index 1f87d0570..b7722989b 100644 --- a/app/components/initEndpoint/initEndpoint.html +++ b/app/components/initEndpoint/initEndpoint.html @@ -107,12 +107,41 @@
- +
- +
- -
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ Required TLS files +
+ +
+ +
{{ formValues.TLSCACert.name }} @@ -122,38 +151,40 @@
- -
- -
- - - {{ formValues.TLSCert.name }} - - - +
+ +
+ +
+ + + {{ formValues.TLSCert.name }} + + + +
-
- - -
- -
- - - {{ formValues.TLSKey.name }} - - - + + +
+ +
+ + + {{ formValues.TLSKey.name }} + + + +
+
-
- +
- +
diff --git a/app/components/initEndpoint/initEndpointController.js b/app/components/initEndpoint/initEndpointController.js index d653f6869..7b72f7f73 100644 --- a/app/components/initEndpoint/initEndpointController.js +++ b/app/components/initEndpoint/initEndpointController.js @@ -2,6 +2,10 @@ angular.module('initEndpoint', []) .controller('InitEndpointController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications) { + if (!_.isEmpty($scope.applicationState.endpoint)) { + $state.go('dashboard'); + } + $scope.logo = StateManager.getState().application.logo; $scope.state = { @@ -13,24 +17,22 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif Name: '', URL: '', TLS: false, + TLSSkipVerify: false, + TLSSKipClientVerify: false, TLSCACert: null, TLSCert: null, TLSKey: null }; - if (!_.isEmpty($scope.applicationState.endpoint)) { - $state.go('dashboard'); - } - - $scope.createLocalEndpoint = function() { $('#createResourceSpinner').show(); var name = 'local'; var URL = 'unix:///var/run/docker.sock'; + var endpointID = 1; EndpointService.createLocalEndpoint(name, URL, false, true) .then(function success(data) { - var endpointID = data.Id; + endpointID = data.Id; EndpointProvider.setEndpointID(endpointID); return StateManager.updateEndpointState(false); }) @@ -38,7 +40,8 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif $state.go('dashboard'); }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint'); + Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); + EndpointService.deleteEndpoint(endpointID); }) .finally(function final() { $('#createResourceSpinner').hide(); @@ -51,13 +54,16 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif var URL = $scope.formValues.URL; var PublicURL = URL.split(':')[0]; var TLS = $scope.formValues.TLS; - var TLSCAFile = $scope.formValues.TLSCACert; - var TLSCertFile = $scope.formValues.TLSCert; - var TLSKeyFile = $scope.formValues.TLSKey; + var TLSSkipVerify = TLS && $scope.formValues.TLSSkipVerify; + var TLSSKipClientVerify = TLS && $scope.formValues.TLSSKipClientVerify; + var TLSCAFile = TLSSkipVerify ? null : $scope.formValues.TLSCACert; + var TLSCertFile = TLSSKipClientVerify ? null : $scope.formValues.TLSCert; + var TLSKeyFile = TLSSKipClientVerify ? null : $scope.formValues.TLSKey; - EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile) + var endpointID = 1; + EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) .then(function success(data) { - var endpointID = data.Id; + endpointID = data.Id; EndpointProvider.setEndpointID(endpointID); return StateManager.updateEndpointState(false); }) @@ -65,7 +71,8 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif $state.go('dashboard'); }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint'); + Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); + EndpointService.deleteEndpoint(endpointID); }) .finally(function final() { $('#createResourceSpinner').hide(); diff --git a/app/directives/endpointSecurity/por-endpoint-security.js b/app/directives/endpointSecurity/por-endpoint-security.js new file mode 100644 index 000000000..51567dc18 --- /dev/null +++ b/app/directives/endpointSecurity/por-endpoint-security.js @@ -0,0 +1,12 @@ +angular.module('portainer').component('porEndpointSecurity', { + templateUrl: 'app/directives/endpointSecurity/porEndpointSecurity.html', + controller: 'porEndpointSecurityController', + bindings: { + // This object will be populated with the form data. + // Model reference in endpointSecurityModel.js + formData: '=', + // The component will use this object to initialize the default values + // if present. + endpoint: '<' + } +}); diff --git a/app/directives/endpointSecurity/porEndpointSecurity.html b/app/directives/endpointSecurity/porEndpointSecurity.html new file mode 100644 index 000000000..ca86ce940 --- /dev/null +++ b/app/directives/endpointSecurity/porEndpointSecurity.html @@ -0,0 +1,126 @@ +
+ +
+
+ + +
+
+ +
+ TLS mode +
+ +
+
+ + You can find out more information about how to protect a Docker environment with TLS in the Docker documentation. + +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ Required TLS files +
+ +
+ +
+ +
+ + + {{ $ctrl.formData.TLSCACert.name }} + + + + +
+
+ + +
+ +
+ +
+ + + {{ $ctrl.formData.TLSCert.name }} + + + + +
+
+ + +
+ +
+ + + {{ $ctrl.formData.TLSKey.name }} + + + + +
+
+ +
+ +
+ +
diff --git a/app/directives/endpointSecurity/porEndpointSecurityController.js b/app/directives/endpointSecurity/porEndpointSecurityController.js new file mode 100644 index 000000000..059903047 --- /dev/null +++ b/app/directives/endpointSecurity/porEndpointSecurityController.js @@ -0,0 +1,32 @@ +angular.module('portainer') +.controller('porEndpointSecurityController', [function () { + var ctrl = this; + + function initComponent() { + if (ctrl.endpoint) { + var endpoint = ctrl.endpoint; + var TLS = endpoint.TLSConfig.TLS; + ctrl.formData.TLS = TLS; + var CACert = endpoint.TLSConfig.TLSCACert; + ctrl.formData.TLSCACert = CACert; + var cert = endpoint.TLSConfig.TLSCert; + ctrl.formData.TLSCert = cert; + var key = endpoint.TLSConfig.TLSKey; + ctrl.formData.TLSKey = key; + + if (TLS) { + if (CACert && cert && key) { + ctrl.formData.TLSMode = 'tls_client_ca'; + } else if (cert && key) { + ctrl.formData.TLSMode = 'tls_client_noca'; + } else if (CACert) { + ctrl.formData.TLSMode = 'tls_ca'; + } else { + ctrl.formData.TLSMode = 'tls_only'; + } + } + } + } + + initComponent(); +}]); diff --git a/app/directives/endpointSecurity/porEndpointSecurityModel.js b/app/directives/endpointSecurity/porEndpointSecurityModel.js new file mode 100644 index 000000000..94f6b0be2 --- /dev/null +++ b/app/directives/endpointSecurity/porEndpointSecurityModel.js @@ -0,0 +1,7 @@ +function EndpointSecurityFormData() { + this.TLS = false; + this.TLSMode = 'tls_client_ca'; + this.TLSCACert = null; + this.TLSCert = null; + this.TLSKey = null; +} diff --git a/app/services/api/endpointService.js b/app/services/api/endpointService.js index 693f9aea9..742b2fc3c 100644 --- a/app/services/api/endpointService.js +++ b/app/services/api/endpointService.js @@ -20,6 +20,8 @@ angular.module('portainer.services') name: endpointParams.name, PublicURL: endpointParams.PublicURL, TLS: endpointParams.TLS, + TLSSkipVerify: endpointParams.TLSSkipVerify, + TLSSkipClientVerify: endpointParams.TLSSkipClientVerify, authorizedUsers: endpointParams.authorizedUsers }; if (endpointParams.type && endpointParams.URL) { @@ -55,18 +57,20 @@ angular.module('portainer.services') return Endpoints.create({}, endpoint).$promise; }; - service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile) { + service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { var endpoint = { Name: name, URL: 'tcp://' + URL, PublicURL: PublicURL, - TLS: TLS + TLS: TLS, + TLSSkipVerify: TLSSkipVerify, + TLSSkipClientVerify: TLSSkipClientVerify }; var deferred = $q.defer(); Endpoints.create({}, endpoint).$promise .then(function success(data) { var endpointID = data.Id; - if (TLS) { + if (!TLSSkipVerify || !TLSSkipClientVerify) { deferred.notify({upload: true}); FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) .then(function success() {