mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +02:00
feat(uac): add multi user management and UAC (#647)
This commit is contained in:
parent
f28f223624
commit
80d50378c5
91 changed files with 3973 additions and 866 deletions
|
@ -33,12 +33,14 @@ const (
|
|||
)
|
||||
|
||||
// NewAuthHandler returns a new instance of AuthHandler.
|
||||
func NewAuthHandler() *AuthHandler {
|
||||
func NewAuthHandler(mw *middleWareService) *AuthHandler {
|
||||
h := &AuthHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.HandleFunc("/auth", h.handlePostAuth)
|
||||
h.Handle("/auth",
|
||||
mw.public(http.HandlerFunc(h.handlePostAuth)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
@ -68,7 +70,7 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
|
|||
var username = req.Username
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.User(username)
|
||||
u, err := handler.UserService.UserByUsername(username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
|
@ -84,7 +86,9 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
username,
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
}
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
@ -18,142 +16,95 @@ import (
|
|||
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||
type DockerHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
proxy http.Handler
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
ProxyFactory ProxyFactory
|
||||
proxies map[portainer.EndpointID]http.Handler
|
||||
}
|
||||
|
||||
// NewDockerHandler returns a new instance of DockerHandler.
|
||||
func NewDockerHandler(middleWareService *middleWareService) *DockerHandler {
|
||||
func NewDockerHandler(mw *middleWareService, resourceControlService portainer.ResourceControlService) *DockerHandler {
|
||||
h := &DockerHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
ProxyFactory: ProxyFactory{
|
||||
ResourceControlService: resourceControlService,
|
||||
},
|
||||
proxies: make(map[portainer.EndpointID]http.Handler),
|
||||
}
|
||||
h.PathPrefix("/").Handler(middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.proxyRequestsToDockerAPI(w, r)
|
||||
})))
|
||||
h.PathPrefix("/{id}/").Handler(
|
||||
mw.authenticated(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
func checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if handler.proxy != nil {
|
||||
handler.proxy.ServeHTTP(w, r)
|
||||
} else {
|
||||
Error(w, portainer.ErrNoActiveEndpoint, http.StatusNotFound, handler.Logger)
|
||||
}
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
func (handler *DockerHandler) setupProxy(endpoint *portainer.Endpoint) error {
|
||||
var proxy http.Handler
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
parsedID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return err
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLS {
|
||||
proxy, err = newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
proxy = newHTTPProxy(endpointURL)
|
||||
}
|
||||
} else {
|
||||
// Assume unix:// scheme
|
||||
proxy = newSocketProxy(endpointURL.Path)
|
||||
}
|
||||
handler.proxy = proxy
|
||||
return nil
|
||||
}
|
||||
|
||||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
||||
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves
|
||||
func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||
req.Host = req.URL.Host
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
// explicitly disable User-Agent so it's not set to default value
|
||||
req.Header.Set("User-Agent", "")
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData.Role != portainer.AdministratorRole && !checkEndpointAccessControl(endpoint, tokenData.ID) {
|
||||
Error(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := handler.proxies[endpointID]
|
||||
if proxy == nil {
|
||||
proxy, err = handler.createAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func newHTTPProxy(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return NewSingleHostReverseProxyWithHostHeader(u)
|
||||
}
|
||||
func (handler *DockerHandler) createAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
var proxy http.Handler
|
||||
|
||||
func newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
u.Scheme = "https"
|
||||
proxy := NewSingleHostReverseProxyWithHostHeader(u)
|
||||
config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: config,
|
||||
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLS {
|
||||
proxy, err = handler.ProxyFactory.newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
proxy = handler.ProxyFactory.newHTTPProxy(endpointURL)
|
||||
}
|
||||
} else {
|
||||
// Assume unix:// scheme
|
||||
proxy = handler.ProxyFactory.newSocketProxy(endpointURL.Path)
|
||||
}
|
||||
|
||||
handler.proxies[endpoint.ID] = proxy
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func newSocketProxy(path string) http.Handler {
|
||||
return &unixSocketHandler{path}
|
||||
}
|
||||
|
||||
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
|
||||
type unixSocketHandler struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := net.Dial("unix", h.path)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
c := httputil.NewClientConn(conn, nil)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Do(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
for k, vv := range res.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
}
|
||||
|
|
121
api/http/docker_proxy.go
Normal file
121
api/http/docker_proxy.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// ProxyFactory is a factory to create reverse proxies to Docker endpoints
|
||||
type ProxyFactory struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
||||
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
|
||||
// It also adds an extra Transport to the proxy to allow Portainer to rewrite the responses.
|
||||
func (factory *ProxyFactory) newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||
req.Host = req.URL.Host
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
// explicitly disable User-Agent so it's not set to default value
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
}
|
||||
transport := &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
transport: &http.Transport{},
|
||||
}
|
||||
return &httputil.ReverseProxy{Director: director, Transport: transport}
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return factory.newSingleHostReverseProxyWithHostHeader(u)
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
u.Scheme = "https"
|
||||
proxy := factory.newSingleHostReverseProxyWithHostHeader(u)
|
||||
config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.Transport.(*proxyTransport).transport.TLSClientConfig = config
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newSocketProxy(path string) http.Handler {
|
||||
return &unixSocketHandler{path, &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
}}
|
||||
}
|
||||
|
||||
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
|
||||
type unixSocketHandler struct {
|
||||
path string
|
||||
transport *proxyTransport
|
||||
}
|
||||
|
||||
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := net.Dial("unix", h.path)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
c := httputil.NewClientConn(conn, nil)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Do(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
err = h.transport.proxyDockerRequests(r, res)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
for k, vv := range res.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
}
|
|
@ -20,8 +20,7 @@ type EndpointHandler struct {
|
|||
authorizeEndpointManagement bool
|
||||
EndpointService portainer.EndpointService
|
||||
FileService portainer.FileService
|
||||
server *Server
|
||||
middleWareService *middleWareService
|
||||
// server *Server
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -31,30 +30,24 @@ const (
|
|||
)
|
||||
|
||||
// NewEndpointHandler returns a new instance of EndpointHandler.
|
||||
func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler {
|
||||
func NewEndpointHandler(mw *middleWareService) *EndpointHandler {
|
||||
h := &EndpointHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostEndpoints(w, r)
|
||||
}))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetEndpoints(w, r)
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetEndpoint(w, r)
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePutEndpoint(w, r)
|
||||
}))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleDeleteEndpoint(w, r)
|
||||
}))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/active", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostEndpoint(w, r)
|
||||
}))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
mw.administrator(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
mw.authenticated(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}/access",
|
||||
mw.administrator(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
@ -65,12 +58,35 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
|
|||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
encodeJSON(w, endpoints, handler.Logger)
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData == nil {
|
||||
Error(w, portainer.ErrInvalidJWTToken, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var allowedEndpoints []portainer.Endpoint
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
allowedEndpoints = make([]portainer.Endpoint, 0)
|
||||
for _, endpoint := range endpoints {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == tokenData.ID {
|
||||
allowedEndpoints = append(allowedEndpoints, endpoint)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
allowedEndpoints = endpoints
|
||||
}
|
||||
|
||||
encodeJSON(w, allowedEndpoints, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostEndpoints handles POST requests on /endpoints
|
||||
// if the active URL parameter is specified, will also define the new endpoint as the active endpoint.
|
||||
// /endpoints(?active=true|false)
|
||||
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
|
@ -90,9 +106,10 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
|||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
TLS: req.TLS,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
TLS: req.TLS,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
|
@ -115,22 +132,6 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
|||
}
|
||||
}
|
||||
|
||||
activeEndpointParameter := r.FormValue("active")
|
||||
if activeEndpointParameter != "" {
|
||||
active, err := strconv.ParseBool(activeEndpointParameter)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
if active == true {
|
||||
err = handler.server.updateActiveEndpoint(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
|
@ -145,7 +146,6 @@ type postEndpointsResponse struct {
|
|||
}
|
||||
|
||||
// handleGetEndpoint handles GET requests on /endpoints/:id
|
||||
// GET /endpoints/0 returns active endpoint
|
||||
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
@ -156,48 +156,6 @@ func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http
|
|||
return
|
||||
}
|
||||
|
||||
var endpoint *portainer.Endpoint
|
||||
if id == "0" {
|
||||
endpoint, err = handler.EndpointService.GetActive()
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if handler.server.ActiveEndpoint == nil {
|
||||
err = handler.server.updateActiveEndpoint(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostEndpoint handles POST requests on /endpoints/:id/active
|
||||
func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
|
@ -207,12 +165,58 @@ func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *htt
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.server.updateActiveEndpoint(endpoint)
|
||||
encodeJSON(w, endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
|
||||
func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointAccessRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range req.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
endpoint.AuthorizedUsers = authorizedUserIDs
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type putEndpointAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"required"`
|
||||
}
|
||||
|
||||
// handlePutEndpoint handles PUT requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
|
@ -241,14 +245,25 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
|||
return
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
TLS: req.TLS,
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
endpoint.Name = req.Name
|
||||
}
|
||||
|
||||
if req.URL != "" {
|
||||
endpoint.URL = req.URL
|
||||
}
|
||||
|
||||
if req.TLS {
|
||||
endpoint.TLS = true
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||
|
@ -256,6 +271,10 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
|||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
} else {
|
||||
endpoint.TLS = false
|
||||
endpoint.TLSCACertPath = ""
|
||||
endpoint.TLSCertPath = ""
|
||||
endpoint.TLSKeyPath = ""
|
||||
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
|
@ -271,13 +290,12 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
|||
}
|
||||
|
||||
type putEndpointsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
TLS bool
|
||||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
TLS bool `valid:"-"`
|
||||
}
|
||||
|
||||
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||
// DELETE /endpoints/0 deletes the active endpoint
|
||||
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
|
@ -293,13 +311,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
|||
return
|
||||
}
|
||||
|
||||
var endpoint *portainer.Endpoint
|
||||
if id == "0" {
|
||||
endpoint, err = handler.EndpointService.GetActive()
|
||||
endpointID = int(endpoint.ID)
|
||||
} else {
|
||||
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
}
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
|
@ -314,13 +326,6 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
|||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if id == "0" {
|
||||
err = handler.EndpointService.DeleteActive()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
|
||||
|
|
|
@ -27,6 +27,10 @@ const (
|
|||
ErrInvalidJSON = portainer.Error("Invalid JSON")
|
||||
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
|
||||
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
|
||||
// ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
|
||||
ErrInvalidQueryFormat = portainer.Error("Invalid query format")
|
||||
// ErrEmptyResponseBody defines an error raised when portainer excepts to parse the body of a HTTP response and there is nothing to parse
|
||||
ErrEmptyResponseBody = portainer.Error("Empty response body")
|
||||
)
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
|
|
|
@ -1,33 +1,61 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Service represents a service to manage HTTP middlewares
|
||||
type middleWareService struct {
|
||||
jwtService portainer.JWTService
|
||||
authDisabled bool
|
||||
}
|
||||
|
||||
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
|
||||
for _, mw := range middleware {
|
||||
h = mw(h)
|
||||
type (
|
||||
// middleWareService represents a service to manage HTTP middlewares
|
||||
middleWareService struct {
|
||||
jwtService portainer.JWTService
|
||||
authDisabled bool
|
||||
}
|
||||
contextKey int
|
||||
)
|
||||
|
||||
const (
|
||||
contextAuthenticationKey contextKey = iota
|
||||
)
|
||||
|
||||
func extractTokenDataFromRequestContext(request *http.Request) (*portainer.TokenData, error) {
|
||||
contextData := request.Context().Value(contextAuthenticationKey)
|
||||
if contextData == nil {
|
||||
return nil, portainer.ErrMissingContextData
|
||||
}
|
||||
|
||||
tokenData := contextData.(*portainer.TokenData)
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
// public defines a chain of middleware for public endpoints (no authentication required)
|
||||
func (service *middleWareService) public(h http.Handler) http.Handler {
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
func (service *middleWareService) addMiddleWares(h http.Handler) http.Handler {
|
||||
h = service.middleWareSecureHeaders(h)
|
||||
h = service.middleWareAuthenticate(h)
|
||||
// authenticated defines a chain of middleware for private endpoints (authentication required)
|
||||
func (service *middleWareService) authenticated(h http.Handler) http.Handler {
|
||||
h = service.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// middleWareAuthenticate provides secure headers middleware for handlers
|
||||
func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handler {
|
||||
// administrator defines a chain of middleware for private administrator restricted endpoints
|
||||
// (authentication and role admin required)
|
||||
func (service *middleWareService) administrator(h http.Handler) http.Handler {
|
||||
h = mwCheckAdministratorRole(h)
|
||||
h = service.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// mwSecureHeaders provides secure headers middleware for handlers
|
||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Add("X-Frame-Options", "DENY")
|
||||
|
@ -35,9 +63,28 @@ func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handle
|
|||
})
|
||||
}
|
||||
|
||||
// middleWareAuthenticate provides Authentication middleware for handlers
|
||||
func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler {
|
||||
// mwCheckAdministratorRole check the role of the user associated to the request
|
||||
func mwCheckAdministratorRole(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// mwCheckAuthentication provides Authentication middleware for handlers
|
||||
func (service *middleWareService) mwCheckAuthentication(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var tokenData *portainer.TokenData
|
||||
if !service.authDisabled {
|
||||
var token string
|
||||
|
||||
|
@ -53,14 +100,20 @@ func (service *middleWareService) middleWareAuthenticate(next http.Handler) http
|
|||
return
|
||||
}
|
||||
|
||||
err := service.jwtService.VerifyToken(token)
|
||||
var err error
|
||||
tokenData, err = service.jwtService.ParseAndVerifyToken(token)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
tokenData = &portainer.TokenData{
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
ctx := context.WithValue(r.Context(), contextAuthenticationKey, tokenData)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
})
|
||||
}
|
||||
|
|
664
api/http/proxy_transport.go
Normal file
664
api/http/proxy_transport.go
Normal file
|
@ -0,0 +1,664 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type (
|
||||
proxyTransport struct {
|
||||
transport *http.Transport
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
resourceControlMetadata struct {
|
||||
OwnerID portainer.UserID `json:"OwnerId"`
|
||||
}
|
||||
)
|
||||
|
||||
func (p *proxyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
response, err := p.transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = p.proxyDockerRequests(req, response)
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyDockerRequests(request *http.Request, response *http.Response) error {
|
||||
path := request.URL.Path
|
||||
|
||||
if strings.HasPrefix(path, "/containers") {
|
||||
return p.handleContainerRequests(request, response)
|
||||
} else if strings.HasPrefix(path, "/services") {
|
||||
return p.handleServiceRequests(request, response)
|
||||
} else if strings.HasPrefix(path, "/volumes") {
|
||||
return p.handleVolumeRequests(request, response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) handleContainerRequests(request *http.Request, response *http.Response) error {
|
||||
requestPath := request.URL.Path
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if requestPath == "/containers/prune" && tokenData.Role != portainer.AdministratorRole {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
if requestPath == "/containers/json" {
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return p.decorateContainerResponse(response)
|
||||
}
|
||||
return p.proxyContainerResponseWithResourceControl(response, tokenData.ID)
|
||||
}
|
||||
// /containers/{id}/action
|
||||
if match, _ := path.Match("/containers/*/*", requestPath); match {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
resourceID := path.Base(path.Dir(requestPath))
|
||||
return p.proxyContainerResponseWithAccessControl(response, tokenData.ID, resourceID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) handleServiceRequests(request *http.Request, response *http.Response) error {
|
||||
requestPath := request.URL.Path
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if requestPath == "/services" {
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return p.decorateServiceResponse(response)
|
||||
}
|
||||
return p.proxyServiceResponseWithResourceControl(response, tokenData.ID)
|
||||
}
|
||||
// /services/{id}
|
||||
if match, _ := path.Match("/services/*", requestPath); match {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
resourceID := path.Base(requestPath)
|
||||
return p.proxyServiceResponseWithAccessControl(response, tokenData.ID, resourceID)
|
||||
}
|
||||
}
|
||||
// /services/{id}/action
|
||||
if match, _ := path.Match("/services/*/*", requestPath); match {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
resourceID := path.Base(path.Dir(requestPath))
|
||||
return p.proxyServiceResponseWithAccessControl(response, tokenData.ID, resourceID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) handleVolumeRequests(request *http.Request, response *http.Response) error {
|
||||
requestPath := request.URL.Path
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if requestPath == "/volumes" {
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return p.decorateVolumeResponse(response)
|
||||
}
|
||||
return p.proxyVolumeResponseWithResourceControl(response, tokenData.ID)
|
||||
}
|
||||
if requestPath == "/volumes/prune" && tokenData.Role != portainer.AdministratorRole {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
// /volumes/{name}
|
||||
if match, _ := path.Match("/volumes/*", requestPath); match {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
resourceID := path.Base(requestPath)
|
||||
return p.proxyVolumeResponseWithAccessControl(response, tokenData.ID, resourceID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyContainerResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyServiceResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyVolumeResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateContainerResponse(response *http.Response) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containers, err := p.decorateContainers(responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteContainerResponse(response, containers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyContainerResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containers, err := p.filterContainers(userID, responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteContainerResponse(response, containers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateServiceResponse(response *http.Response) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
services, err := p.decorateServices(responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteServiceResponse(response, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyServiceResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumes, err := p.filterServices(userID, responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteServiceResponse(response, volumes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateVolumeResponse(response *http.Response) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumes, err := p.decorateVolumes(responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteVolumeResponse(response, volumes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyVolumeResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumes, err := p.filterVolumes(userID, responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteVolumeResponse(response, volumes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateContainers(responseData interface{}) ([]interface{}, error) {
|
||||
responseDataArray := responseData.([]interface{})
|
||||
|
||||
containerRCs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceRCs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decoratedResources := make([]interface{}, 0)
|
||||
|
||||
for _, container := range responseDataArray {
|
||||
jsonObject := container.(map[string]interface{})
|
||||
containerID := jsonObject["Id"].(string)
|
||||
containerRC := getRCByResourceID(containerID, containerRCs)
|
||||
if containerRC != nil {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonObject, containerRC.OwnerID)
|
||||
decoratedResources = append(decoratedResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
|
||||
containerLabels := jsonObject["Labels"]
|
||||
if containerLabels != nil {
|
||||
jsonLabels := containerLabels.(map[string]interface{})
|
||||
serviceID := jsonLabels["com.docker.swarm.service.id"]
|
||||
if serviceID != nil {
|
||||
serviceRC := getRCByResourceID(serviceID.(string), serviceRCs)
|
||||
if serviceRC != nil {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonObject, serviceRC.OwnerID)
|
||||
decoratedResources = append(decoratedResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
decoratedResources = append(decoratedResources, container)
|
||||
}
|
||||
|
||||
return decoratedResources, nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) filterContainers(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
|
||||
responseDataArray := responseData.([]interface{})
|
||||
|
||||
containerRCs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceRCs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOwnedContainerIDs, err := getResourceIDsOwnedByUser(userID, containerRCs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOwnedServiceIDs, err := getResourceIDsOwnedByUser(userID, serviceRCs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicContainers := getPublicContainers(responseDataArray, containerRCs, serviceRCs)
|
||||
|
||||
filteredResources := make([]interface{}, 0)
|
||||
|
||||
for _, container := range responseDataArray {
|
||||
jsonObject := container.(map[string]interface{})
|
||||
containerID := jsonObject["Id"].(string)
|
||||
if isStringInArray(containerID, userOwnedContainerIDs) {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonObject, userID)
|
||||
filteredResources = append(filteredResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
|
||||
containerLabels := jsonObject["Labels"]
|
||||
if containerLabels != nil {
|
||||
jsonLabels := containerLabels.(map[string]interface{})
|
||||
serviceID := jsonLabels["com.docker.swarm.service.id"]
|
||||
if serviceID != nil && isStringInArray(serviceID.(string), userOwnedServiceIDs) {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonObject, userID)
|
||||
filteredResources = append(filteredResources, decoratedObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filteredResources = append(filteredResources, publicContainers...)
|
||||
return filteredResources, nil
|
||||
}
|
||||
|
||||
func decorateWithResourceControlMetadata(object map[string]interface{}, userID portainer.UserID) map[string]interface{} {
|
||||
metadata := make(map[string]interface{})
|
||||
metadata["ResourceControl"] = resourceControlMetadata{
|
||||
OwnerID: userID,
|
||||
}
|
||||
object["Portainer"] = metadata
|
||||
return object
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateServices(responseData interface{}) ([]interface{}, error) {
|
||||
responseDataArray := responseData.([]interface{})
|
||||
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decoratedResources := make([]interface{}, 0)
|
||||
|
||||
for _, service := range responseDataArray {
|
||||
jsonResource := service.(map[string]interface{})
|
||||
resourceID := jsonResource["ID"].(string)
|
||||
serviceRC := getRCByResourceID(resourceID, rcs)
|
||||
if serviceRC != nil {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonResource, serviceRC.OwnerID)
|
||||
decoratedResources = append(decoratedResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
decoratedResources = append(decoratedResources, service)
|
||||
}
|
||||
|
||||
return decoratedResources, nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) filterServices(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
|
||||
responseDataArray := responseData.([]interface{})
|
||||
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOwnedServiceIDs, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicServices := getPublicResources(responseDataArray, rcs, "ID")
|
||||
|
||||
filteredResources := make([]interface{}, 0)
|
||||
|
||||
for _, res := range responseDataArray {
|
||||
jsonResource := res.(map[string]interface{})
|
||||
resourceID := jsonResource["ID"].(string)
|
||||
if isStringInArray(resourceID, userOwnedServiceIDs) {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonResource, userID)
|
||||
filteredResources = append(filteredResources, decoratedObject)
|
||||
}
|
||||
}
|
||||
|
||||
filteredResources = append(filteredResources, publicServices...)
|
||||
return filteredResources, nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateVolumes(responseData interface{}) ([]interface{}, error) {
|
||||
var responseDataArray []interface{}
|
||||
jsonObject := responseData.(map[string]interface{})
|
||||
if jsonObject["Volumes"] != nil {
|
||||
responseDataArray = jsonObject["Volumes"].([]interface{})
|
||||
}
|
||||
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decoratedResources := make([]interface{}, 0)
|
||||
|
||||
for _, volume := range responseDataArray {
|
||||
jsonResource := volume.(map[string]interface{})
|
||||
resourceID := jsonResource["Name"].(string)
|
||||
volumeRC := getRCByResourceID(resourceID, rcs)
|
||||
if volumeRC != nil {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonResource, volumeRC.OwnerID)
|
||||
decoratedResources = append(decoratedResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
decoratedResources = append(decoratedResources, volume)
|
||||
}
|
||||
|
||||
return decoratedResources, nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) filterVolumes(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
|
||||
var responseDataArray []interface{}
|
||||
jsonObject := responseData.(map[string]interface{})
|
||||
if jsonObject["Volumes"] != nil {
|
||||
responseDataArray = jsonObject["Volumes"].([]interface{})
|
||||
}
|
||||
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOwnedVolumeIDs, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicVolumes := getPublicResources(responseDataArray, rcs, "Name")
|
||||
|
||||
filteredResources := make([]interface{}, 0)
|
||||
|
||||
for _, res := range responseDataArray {
|
||||
jsonResource := res.(map[string]interface{})
|
||||
resourceID := jsonResource["Name"].(string)
|
||||
if isStringInArray(resourceID, userOwnedVolumeIDs) {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonResource, userID)
|
||||
filteredResources = append(filteredResources, decoratedObject)
|
||||
}
|
||||
}
|
||||
|
||||
filteredResources = append(filteredResources, publicVolumes...)
|
||||
return filteredResources, nil
|
||||
}
|
||||
|
||||
func getResourceIDsOwnedByUser(userID portainer.UserID, rcs []portainer.ResourceControl) ([]string, error) {
|
||||
ownedResources := make([]string, 0)
|
||||
for _, rc := range rcs {
|
||||
if rc.OwnerID == userID {
|
||||
ownedResources = append(ownedResources, rc.ResourceID)
|
||||
}
|
||||
}
|
||||
return ownedResources, nil
|
||||
}
|
||||
|
||||
func getOwnedServiceContainers(responseData []interface{}, serviceRCs []portainer.ResourceControl) []interface{} {
|
||||
ownedContainers := make([]interface{}, 0)
|
||||
for _, res := range responseData {
|
||||
jsonResource := res.(map[string]map[string]interface{})
|
||||
swarmServiceID := jsonResource["Labels"]["com.docker.swarm.service.id"]
|
||||
if swarmServiceID != nil {
|
||||
resourceID := swarmServiceID.(string)
|
||||
if isResourceIDInRCs(resourceID, serviceRCs) {
|
||||
ownedContainers = append(ownedContainers, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ownedContainers
|
||||
}
|
||||
|
||||
func getPublicContainers(responseData []interface{}, containerRCs []portainer.ResourceControl, serviceRCs []portainer.ResourceControl) []interface{} {
|
||||
publicContainers := make([]interface{}, 0)
|
||||
for _, container := range responseData {
|
||||
jsonObject := container.(map[string]interface{})
|
||||
containerID := jsonObject["Id"].(string)
|
||||
if !isResourceIDInRCs(containerID, containerRCs) {
|
||||
containerLabels := jsonObject["Labels"]
|
||||
if containerLabels != nil {
|
||||
jsonLabels := containerLabels.(map[string]interface{})
|
||||
serviceID := jsonLabels["com.docker.swarm.service.id"]
|
||||
if serviceID == nil {
|
||||
publicContainers = append(publicContainers, container)
|
||||
} else if serviceID != nil && !isResourceIDInRCs(serviceID.(string), serviceRCs) {
|
||||
publicContainers = append(publicContainers, container)
|
||||
}
|
||||
} else {
|
||||
publicContainers = append(publicContainers, container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return publicContainers
|
||||
}
|
||||
|
||||
func getPublicResources(responseData []interface{}, rcs []portainer.ResourceControl, resourceIDKey string) []interface{} {
|
||||
publicResources := make([]interface{}, 0)
|
||||
for _, res := range responseData {
|
||||
jsonResource := res.(map[string]interface{})
|
||||
resourceID := jsonResource[resourceIDKey].(string)
|
||||
if !isResourceIDInRCs(resourceID, rcs) {
|
||||
publicResources = append(publicResources, res)
|
||||
}
|
||||
}
|
||||
return publicResources
|
||||
}
|
||||
|
||||
func isStringInArray(target string, array []string) bool {
|
||||
for _, element := range array {
|
||||
if element == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isResourceIDInRCs(resourceID string, rcs []portainer.ResourceControl) bool {
|
||||
for _, rc := range rcs {
|
||||
if resourceID == rc.ResourceID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getRCByResourceID(resourceID string, rcs []portainer.ResourceControl) *portainer.ResourceControl {
|
||||
for _, rc := range rcs {
|
||||
if resourceID == rc.ResourceID {
|
||||
return &rc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getResponseData(response *http.Response) (interface{}, error) {
|
||||
var data interface{}
|
||||
if response.Body != nil {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
return nil, ErrEmptyResponseBody
|
||||
}
|
||||
|
||||
func writeAccessDeniedResponse(response *http.Response) error {
|
||||
return rewriteResponse(response, portainer.ErrResourceAccessDenied, 403)
|
||||
}
|
||||
|
||||
func rewriteContainerResponse(response *http.Response, responseData interface{}) error {
|
||||
return rewriteResponse(response, responseData, 200)
|
||||
}
|
||||
|
||||
func rewriteServiceResponse(response *http.Response, responseData interface{}) error {
|
||||
return rewriteResponse(response, responseData, 200)
|
||||
}
|
||||
|
||||
func rewriteVolumeResponse(response *http.Response, responseData interface{}) error {
|
||||
data := map[string]interface{}{}
|
||||
data["Volumes"] = responseData
|
||||
return rewriteResponse(response, data, 200)
|
||||
}
|
||||
|
||||
func rewriteResponse(response *http.Response, newContent interface{}, statusCode int) error {
|
||||
jsonData, err := json.Marshal(newContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := ioutil.NopCloser(bytes.NewReader(jsonData))
|
||||
response.StatusCode = statusCode
|
||||
response.Body = body
|
||||
response.ContentLength = int64(len(jsonData))
|
||||
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
|
||||
return nil
|
||||
}
|
|
@ -8,35 +8,19 @@ import (
|
|||
|
||||
// Server implements the portainer.Server interface
|
||||
type Server struct {
|
||||
BindAddress string
|
||||
AssetsPath string
|
||||
AuthDisabled bool
|
||||
EndpointManagement bool
|
||||
UserService portainer.UserService
|
||||
EndpointService portainer.EndpointService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
FileService portainer.FileService
|
||||
Settings *portainer.Settings
|
||||
TemplatesURL string
|
||||
ActiveEndpoint *portainer.Endpoint
|
||||
Handler *Handler
|
||||
}
|
||||
|
||||
func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
|
||||
if endpoint != nil {
|
||||
server.ActiveEndpoint = endpoint
|
||||
server.Handler.WebSocketHandler.endpoint = endpoint
|
||||
err := server.Handler.DockerHandler.setupProxy(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = server.EndpointService.SetActive(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
BindAddress string
|
||||
AssetsPath string
|
||||
AuthDisabled bool
|
||||
EndpointManagement bool
|
||||
UserService portainer.UserService
|
||||
EndpointService portainer.EndpointService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
FileService portainer.FileService
|
||||
Settings *portainer.Settings
|
||||
TemplatesURL string
|
||||
Handler *Handler
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
|
@ -46,7 +30,7 @@ func (server *Server) Start() error {
|
|||
authDisabled: server.AuthDisabled,
|
||||
}
|
||||
|
||||
var authHandler = NewAuthHandler()
|
||||
var authHandler = NewAuthHandler(middleWareService)
|
||||
authHandler.UserService = server.UserService
|
||||
authHandler.CryptoService = server.CryptoService
|
||||
authHandler.JWTService = server.JWTService
|
||||
|
@ -54,18 +38,19 @@ func (server *Server) Start() error {
|
|||
var userHandler = NewUserHandler(middleWareService)
|
||||
userHandler.UserService = server.UserService
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
userHandler.ResourceControlService = server.ResourceControlService
|
||||
var settingsHandler = NewSettingsHandler(middleWareService)
|
||||
settingsHandler.settings = server.Settings
|
||||
var templatesHandler = NewTemplatesHandler(middleWareService)
|
||||
templatesHandler.templatesURL = server.TemplatesURL
|
||||
var dockerHandler = NewDockerHandler(middleWareService)
|
||||
var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
|
||||
dockerHandler.EndpointService = server.EndpointService
|
||||
var websocketHandler = NewWebSocketHandler()
|
||||
// EndpointHandler requires a reference to the server to be able to update the active endpoint.
|
||||
websocketHandler.EndpointService = server.EndpointService
|
||||
var endpointHandler = NewEndpointHandler(middleWareService)
|
||||
endpointHandler.authorizeEndpointManagement = server.EndpointManagement
|
||||
endpointHandler.EndpointService = server.EndpointService
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.server = server
|
||||
var uploadHandler = NewUploadHandler(middleWareService)
|
||||
uploadHandler.FileService = server.FileService
|
||||
var fileHandler = newFileHandler(server.AssetsPath)
|
||||
|
@ -81,10 +66,6 @@ func (server *Server) Start() error {
|
|||
FileHandler: fileHandler,
|
||||
UploadHandler: uploadHandler,
|
||||
}
|
||||
err := server.updateActiveEndpoint(server.ActiveEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return http.ListenAndServe(server.BindAddress, server.Handler)
|
||||
}
|
||||
|
|
|
@ -13,19 +13,19 @@ import (
|
|||
// SettingsHandler represents an HTTP API handler for managing settings.
|
||||
type SettingsHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
settings *portainer.Settings
|
||||
Logger *log.Logger
|
||||
settings *portainer.Settings
|
||||
}
|
||||
|
||||
// NewSettingsHandler returns a new instance of SettingsHandler.
|
||||
func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler {
|
||||
func NewSettingsHandler(mw *middleWareService) *SettingsHandler {
|
||||
h := &SettingsHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.HandleFunc("/settings", h.handleGetSettings)
|
||||
h.Handle("/settings",
|
||||
mw.public(http.HandlerFunc(h.handleGetSettings)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
|
|
@ -12,21 +12,18 @@ import (
|
|||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||
type TemplatesHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
templatesURL string
|
||||
Logger *log.Logger
|
||||
templatesURL string
|
||||
}
|
||||
|
||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||
func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler {
|
||||
func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
|
||||
h := &TemplatesHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/templates", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetTemplates(w, r)
|
||||
})))
|
||||
h.Handle("/templates",
|
||||
mw.authenticated(http.HandlerFunc(h.handleGetTemplates)))
|
||||
return h
|
||||
}
|
||||
|
||||
|
|
|
@ -14,21 +14,18 @@ import (
|
|||
// UploadHandler represents an HTTP API handler for managing file uploads.
|
||||
type UploadHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
FileService portainer.FileService
|
||||
middleWareService *middleWareService
|
||||
Logger *log.Logger
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewUploadHandler returns a new instance of UploadHandler.
|
||||
func NewUploadHandler(middleWareService *middleWareService) *UploadHandler {
|
||||
func NewUploadHandler(mw *middleWareService) *UploadHandler {
|
||||
h := &UploadHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUploadTLS(w, r)
|
||||
})))
|
||||
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePostUploadTLS)))
|
||||
return h
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
|
@ -15,43 +17,44 @@ import (
|
|||
// UserHandler represents an HTTP API handler for managing users.
|
||||
type UserHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
middleWareService *middleWareService
|
||||
Logger *log.Logger
|
||||
UserService portainer.UserService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
}
|
||||
|
||||
// NewUserHandler returns a new instance of UserHandler.
|
||||
func NewUserHandler(middleWareService *middleWareService) *UserHandler {
|
||||
func NewUserHandler(mw *middleWareService) *UserHandler {
|
||||
h := &UserHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/users", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUsers(w, r)
|
||||
})))
|
||||
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetUser(w, r)
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePutUser(w, r)
|
||||
}))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUserPasswd(w, r)
|
||||
})))
|
||||
h.HandleFunc("/users/admin/check", h.handleGetAdminCheck)
|
||||
h.HandleFunc("/users/admin/init", h.handlePostAdminInit)
|
||||
h.Handle("/users",
|
||||
mw.administrator(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
|
||||
h.Handle("/users",
|
||||
mw.administrator(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/passwd",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePostUserPasswd)))
|
||||
h.Handle("/users/{userId}/resources/{resourceType}",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePostUserResource))).Methods(http.MethodPost)
|
||||
h.Handle("/users/{userId}/resources/{resourceType}/{resourceId}",
|
||||
mw.authenticated(http.HandlerFunc(h.handleDeleteUserResource))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/admin/check",
|
||||
mw.public(http.HandlerFunc(h.handleGetAdminCheck)))
|
||||
h.Handle("/users/admin/init",
|
||||
mw.public(http.HandlerFunc(h.handlePostAdminInit)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// handlePostUsers handles POST requests on /users
|
||||
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
var req postUsersRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
|
@ -64,8 +67,26 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
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 {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
Error(w, portainer.ErrUserAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: req.Username,
|
||||
Role: role,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
|
@ -73,7 +94,7 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -83,9 +104,24 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
|||
type postUsersRequest struct {
|
||||
Username string `valid:"alphanum,required"`
|
||||
Password string `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
// handlePostUserPasswd handles POST requests on /users/:username/passwd
|
||||
// handleGetUsers handles GET requests on /users
|
||||
func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := handler.UserService.Users()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
encodeJSON(w, users, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostUserPasswd handles POST requests on /users/:id/passwd
|
||||
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
|
@ -93,15 +129,21 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
|
|||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postUserPasswdRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
|
@ -109,7 +151,7 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
|
|||
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.User(username)
|
||||
u, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
|
@ -135,12 +177,18 @@ type postUserPasswdResponse struct {
|
|||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
// handleGetUser handles GET requests on /users/:username
|
||||
// handleGetUser handles GET requests on /users/:id
|
||||
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
id := vars["id"]
|
||||
|
||||
user, err := handler.UserService.User(username)
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
|
@ -153,30 +201,74 @@ func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request
|
|||
encodeJSON(w, &user, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutUser handles PUT requests on /users/:username
|
||||
// handlePutUser handles PUT requests on /users/:id
|
||||
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
|
||||
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: req.Username,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
if req.Password == "" && req.Role == 0 {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password != "" {
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Role != 0 {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
if req.Role == 1 {
|
||||
user.Role = portainer.AdministratorRole
|
||||
} else {
|
||||
user.Role = portainer.StandardUserRole
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user.ID, user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -184,8 +276,8 @@ func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
|
||||
type putUserRequest struct {
|
||||
Username string `valid:"alphanum,required"`
|
||||
Password string `valid:"required"`
|
||||
Password string `valid:"-"`
|
||||
Role int `valid:"-"`
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles GET requests on /users/admin/check
|
||||
|
@ -195,17 +287,15 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
encodeJSON(w, &user, handler.Logger)
|
||||
if len(users) == 0 {
|
||||
Error(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||
|
@ -227,10 +317,11 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User("admin")
|
||||
user, err := handler.UserService.UserByUsername("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
|
@ -238,7 +329,7 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -256,3 +347,134 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
|||
type postAdminInitRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
// handleDeleteUser handles DELETE requests on /users/:id
|
||||
func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.DeleteUser(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePostUserResource handles POST requests on /users/:userId/resources/:resourceType
|
||||
func (handler *UserHandler) handlePostUserResource(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["userId"]
|
||||
resourceType := vars["resourceType"]
|
||||
|
||||
uid, err := strconv.Atoi(userID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var rcType portainer.ResourceControlType
|
||||
if resourceType == "container" {
|
||||
rcType = portainer.ContainerResourceControl
|
||||
} else if resourceType == "service" {
|
||||
rcType = portainer.ServiceResourceControl
|
||||
} else if resourceType == "volume" {
|
||||
rcType = portainer.VolumeResourceControl
|
||||
} else {
|
||||
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData.ID != portainer.UserID(uid) {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postUserResourceRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resource := portainer.ResourceControl{
|
||||
OwnerID: portainer.UserID(uid),
|
||||
ResourceID: req.ResourceID,
|
||||
AccessLevel: portainer.RestrictedResourceAccessLevel,
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.CreateResourceControl(req.ResourceID, &resource, rcType)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type postUserResourceRequest struct {
|
||||
ResourceID string `valid:"required"`
|
||||
}
|
||||
|
||||
// handleDeleteUserResource handles DELETE requests on /users/:userId/resources/:resourceType/:resourceId
|
||||
func (handler *UserHandler) handleDeleteUserResource(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["userId"]
|
||||
resourceID := vars["resourceId"]
|
||||
resourceType := vars["resourceType"]
|
||||
|
||||
uid, err := strconv.Atoi(userID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var rcType portainer.ResourceControlType
|
||||
if resourceType == "container" {
|
||||
rcType = portainer.ContainerResourceControl
|
||||
} else if resourceType == "service" {
|
||||
rcType = portainer.ServiceResourceControl
|
||||
} else if resourceType == "volume" {
|
||||
rcType = portainer.VolumeResourceControl
|
||||
} else {
|
||||
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(uid) {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.DeleteResourceControl(resourceID, rcType)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
|
@ -14,18 +12,19 @@ import (
|
|||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
|
||||
type WebSocketHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
endpoint *portainer.Endpoint
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
}
|
||||
|
||||
// NewWebSocketHandler returns a new instance of WebSocketHandler.
|
||||
|
@ -41,34 +40,47 @@ func NewWebSocketHandler() *WebSocketHandler {
|
|||
func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||
qry := ws.Request().URL.Query()
|
||||
execID := qry.Get("id")
|
||||
edpID := qry.Get("endpointId")
|
||||
|
||||
// Should not be managed here
|
||||
endpoint, err := url.Parse(handler.endpoint.URL)
|
||||
parsedID, err := strconv.Atoi(edpID)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to parse endpoint URL: %s", err)
|
||||
log.Printf("Unable to parse endpoint ID: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
log.Printf("Unable to retrieve endpoint: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
log.Printf("Unable to parse endpoint URL: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
var host string
|
||||
if endpoint.Scheme == "tcp" {
|
||||
host = endpoint.Host
|
||||
} else if endpoint.Scheme == "unix" {
|
||||
host = endpoint.Path
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
host = endpointURL.Host
|
||||
} else if endpointURL.Scheme == "unix" {
|
||||
host = endpointURL.Path
|
||||
}
|
||||
|
||||
// Should not be managed here
|
||||
var tlsConfig *tls.Config
|
||||
if handler.endpoint.TLS {
|
||||
tlsConfig, err = createTLSConfiguration(handler.endpoint.TLSCACertPath,
|
||||
handler.endpoint.TLSCertPath,
|
||||
handler.endpoint.TLSKeyPath)
|
||||
if endpoint.TLS {
|
||||
tlsConfig, err = createTLSConfiguration(endpoint.TLSCACertPath,
|
||||
endpoint.TLSCertPath,
|
||||
endpoint.TLSKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := hijack(host, endpoint.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||
if err := hijack(host, endpointURL.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||
log.Fatalf("error during hijack: %s", err)
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue