1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +02:00

feat(storidge): introduce endpoint extensions and proxy Storidge API (#1661)

This commit is contained in:
Anthony Lapenna 2018-02-23 03:10:26 +01:00 committed by GitHub
parent b5e256c967
commit eb43579378
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 571 additions and 372 deletions

View file

@ -35,24 +35,6 @@ func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
return h
}
func (handler *DockerHandler) checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
for _, authorizedUserID := range endpoint.AuthorizedUsers {
if authorizedUserID == userID {
return true
}
}
memberships, _ := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
for _, membership := range memberships {
if membership.TeamID == authorizedTeamID {
return true
}
}
}
return false
}
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
@ -75,7 +57,14 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && !handler.checkEndpointAccessControl(endpoint, tokenData.ID) {
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
@ -85,7 +74,7 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}

View file

@ -136,6 +136,7 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
err = handler.EndpointService.CreateEndpoint(endpoint)
@ -372,6 +373,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
}
handler.ProxyManager.DeleteProxy(string(endpointID))
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {

View file

@ -0,0 +1,99 @@
package handler
import (
"encoding/json"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// ExtensionHandler represents an HTTP API handler for managing Settings.
type ExtensionHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
ProxyManager *proxy.Manager
}
// NewExtensionHandler returns a new instance of ExtensionHandler.
func NewExtensionHandler(bouncer *security.RequestBouncer) *ExtensionHandler {
h := &ExtensionHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/{endpointId}/extensions",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostExtensions))).Methods(http.MethodPost)
return h
}
type (
postExtensionRequest struct {
Type int `valid:"required"`
URL string `valid:"required"`
}
)
func (handler *ExtensionHandler) handlePostExtensions(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["endpointId"])
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(id)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
var req postExtensionRequest
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
}
extensionType := portainer.EndpointExtensionType(req.Type)
for _, extension := range endpoint.Extensions {
if extension.Type == extensionType {
httperror.WriteErrorResponse(w, portainer.ErrEndpointExtensionAlreadyAssociated, http.StatusConflict, handler.Logger)
return
}
}
extension := portainer.EndpointExtension{
Type: extensionType,
URL: req.URL,
}
endpoint.Extensions = append(endpoint.Extensions, extension)
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, extension, handler.Logger)
}

View file

@ -0,0 +1,97 @@
package extensions
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// StoridgeHandler represents an HTTP API handler for proxying requests to the Docker API.
type StoridgeHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
TeamMembershipService portainer.TeamMembershipService
ProxyManager *proxy.Manager
}
// NewStoridgeHandler returns a new instance of StoridgeHandler.
func NewStoridgeHandler(bouncer *security.RequestBouncer) *StoridgeHandler {
h := &StoridgeHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.PathPrefix("/{id}/extensions/storidge").Handler(
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToStoridgeAPI)))
return h
}
func (handler *StoridgeHandler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
parsedID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
var storidgeExtension *portainer.EndpointExtension
for _, extension := range endpoint.Extensions {
if extension.Type == portainer.StoridgeEndpointExtension {
storidgeExtension = &extension
}
}
if storidgeExtension == nil {
httperror.WriteErrorResponse(w, portainer.ErrEndpointExtensionNotSupported, http.StatusInternalServerError, handler.Logger)
return
}
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
var proxy http.Handler
proxy = handler.ProxyManager.GetExtensionProxy(proxyExtensionKey)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r)
}

View file

@ -8,6 +8,7 @@ import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/handler/extensions"
)
// Handler is a collection of all the service handlers.
@ -19,6 +20,8 @@ type Handler struct {
EndpointHandler *EndpointHandler
RegistryHandler *RegistryHandler
DockerHubHandler *DockerHubHandler
ExtensionHandler *ExtensionHandler
StoridgeHandler *extensions.StoridgeHandler
ResourceHandler *ResourceHandler
StackHandler *StackHandler
StatusHandler *StatusHandler
@ -48,11 +51,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
if strings.Contains(r.URL.Path, "/docker/") {
switch {
case strings.Contains(r.URL.Path, "/docker"):
http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)
} else if strings.Contains(r.URL.Path, "/stacks") {
case strings.Contains(r.URL.Path, "/stacks"):
http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r)
} else {
case strings.Contains(r.URL.Path, "/extensions/storidge"):
http.StripPrefix("/api/endpoints", h.StoridgeHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/extensions"):
http.StripPrefix("/api/endpoints", h.ExtensionHandler).ServeHTTP(w, r)
default:
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
}
case strings.HasPrefix(r.URL.Path, "/api/registries"):

View file

@ -17,14 +17,14 @@ type proxyFactory struct {
SettingsService portainer.SettingsService
}
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
func (factory *proxyFactory) newExtensionHTTPPRoxy(u *url.URL) http.Handler {
u.Scheme = "http"
return factory.createReverseProxy(u)
return newSingleHostReverseProxyWithHostHeader(u)
}
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
u.Scheme = "https"
proxy := factory.createReverseProxy(u)
proxy := factory.createDockerReverseProxy(u)
config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
if err != nil {
return nil, err
@ -34,7 +34,12 @@ func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpo
return proxy, nil
}
func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL) http.Handler {
u.Scheme = "http"
return factory.createDockerReverseProxy(u)
}
func (factory *proxyFactory) newDockerSocketProxy(path string) http.Handler {
proxy := &socketProxy{}
transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService,
@ -46,13 +51,13 @@ func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
return proxy
}
func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReverseProxy {
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL) *httputil.ReverseProxy {
proxy := newSingleHostReverseProxyWithHostHeader(u)
transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService,
TeamMembershipService: factory.TeamMembershipService,
SettingsService: factory.SettingsService,
dockerTransport: newHTTPTransport(),
dockerTransport: &http.Transport{},
}
proxy.Transport = transport
return proxy
@ -65,7 +70,3 @@ func newSocketTransport(socketPath string) *http.Transport {
},
}
}
func newHTTPTransport() *http.Transport {
return &http.Transport{}
}

View file

@ -3,6 +3,7 @@ package proxy
import (
"net/http"
"net/url"
"strings"
"github.com/orcaman/concurrent-map"
"github.com/portainer/portainer"
@ -10,14 +11,16 @@ import (
// Manager represents a service used to manage Docker proxies.
type Manager struct {
proxyFactory *proxyFactory
proxies cmap.ConcurrentMap
proxyFactory *proxyFactory
proxies cmap.ConcurrentMap
extensionProxies cmap.ConcurrentMap
}
// NewManager initializes a new proxy Service
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager {
return &Manager{
proxies: cmap.New(),
proxies: cmap.New(),
extensionProxies: cmap.New(),
proxyFactory: &proxyFactory{
ResourceControlService: resourceControlService,
TeamMembershipService: teamMembershipService,
@ -38,16 +41,16 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
if endpointURL.Scheme == "tcp" {
if endpoint.TLSConfig.TLS {
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
proxy, err = manager.proxyFactory.newDockerHTTPSProxy(endpointURL, endpoint)
if err != nil {
return nil, err
}
} else {
proxy = manager.proxyFactory.newHTTPProxy(endpointURL)
proxy = manager.proxyFactory.newDockerHTTPProxy(endpointURL)
}
} else {
// Assume unix:// scheme
proxy = manager.proxyFactory.newSocketProxy(endpointURL.Path)
proxy = manager.proxyFactory.newDockerSocketProxy(endpointURL.Path)
}
manager.proxies.Set(string(endpoint.ID), proxy)
@ -67,3 +70,34 @@ func (manager *Manager) GetProxy(key string) http.Handler {
func (manager *Manager) DeleteProxy(key string) {
manager.proxies.Remove(key)
}
// CreateAndRegisterExtensionProxy creates a new HTTP reverse proxy for an extension and adds it to the registered proxies.
func (manager *Manager) CreateAndRegisterExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
extensionURL, err := url.Parse(extensionAPIURL)
if err != nil {
return nil, err
}
proxy := manager.proxyFactory.newExtensionHTTPPRoxy(extensionURL)
manager.extensionProxies.Set(key, proxy)
return proxy, nil
}
// GetExtensionProxy returns the extension proxy associated to a key
func (manager *Manager) GetExtensionProxy(key string) http.Handler {
proxy, ok := manager.extensionProxies.Get(key)
if !ok {
return nil
}
return proxy.(http.Handler)
}
// DeleteExtensionProxies deletes all the extension proxies associated to a key
func (manager *Manager) DeleteExtensionProxies(key string) {
for _, k := range manager.extensionProxies.Keys() {
if strings.Contains(k, key+"_") {
manager.extensionProxies.Remove(k)
}
}
}

View file

@ -121,3 +121,22 @@ func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedReques
}
return false
}
// AuthorizedEndpointAccess ensure that the user can access the specified endpoint.
// It will check if the user is part of the authorized users or part of a team that is
// listed in the authorized teams.
func AuthorizedEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
for _, authorizedUserID := range endpoint.AuthorizedUsers {
if authorizedUserID == userID {
return true
}
}
for _, membership := range memberships {
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
if membership.TeamID == authorizedTeamID {
return true
}
}
}
return false
}

View file

@ -3,6 +3,7 @@ package http
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/handler"
"github.com/portainer/portainer/http/handler/extensions"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
@ -96,6 +97,13 @@ func (server *Server) Start() error {
stackHandler.GitService = server.GitService
stackHandler.RegistryService = server.RegistryService
stackHandler.DockerHubService = server.DockerHubService
var extensionHandler = handler.NewExtensionHandler(requestBouncer)
extensionHandler.EndpointService = server.EndpointService
extensionHandler.ProxyManager = proxyManager
var storidgeHandler = extensions.NewStoridgeHandler(requestBouncer)
storidgeHandler.EndpointService = server.EndpointService
storidgeHandler.TeamMembershipService = server.TeamMembershipService
storidgeHandler.ProxyManager = proxyManager
server.Handler = &handler.Handler{
AuthHandler: authHandler,
@ -114,6 +122,8 @@ func (server *Server) Start() error {
WebSocketHandler: websocketHandler,
FileHandler: fileHandler,
UploadHandler: uploadHandler,
ExtensionHandler: extensionHandler,
StoridgeHandler: storidgeHandler,
}
if server.SSL {