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

feat(global): introduce user teams and new UAC system (#868)

This commit is contained in:
Anthony Lapenna 2017-05-23 20:56:10 +02:00 committed by GitHub
parent a380fd9adc
commit 5523fc9023
160 changed files with 7112 additions and 3166 deletions

View file

@ -1,80 +0,0 @@
package http
import (
"strconv"
"github.com/portainer/portainer"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
type DockerHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
ProxyService *ProxyService
}
// NewDockerHandler returns a new instance of DockerHandler.
func NewDockerHandler(mw *middleWareService, resourceControlService portainer.ResourceControlService) *DockerHandler {
h := &DockerHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
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) {
vars := mux.Vars(r)
id := vars["id"]
parsedID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
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
}
var proxy http.Handler
proxy = handler.ProxyService.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyService.CreateAndRegisterProxy(endpoint)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
}
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
}

View file

@ -1,121 +0,0 @@
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)
}
}

30
api/http/error/error.go Normal file
View file

@ -0,0 +1,30 @@
package error
import (
"encoding/json"
"log"
"net/http"
"strings"
)
// errorResponse is a generic response for sending a error.
type errorResponse struct {
Err string `json:"err,omitempty"`
}
// WriteErrorResponse writes an error message to the response and logger.
func WriteErrorResponse(w http.ResponseWriter, err error, code int, logger *log.Logger) {
if logger != nil {
logger.Printf("http error: %s (code=%d)", err, code)
}
w.WriteHeader(code)
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
}
// WriteMethodNotAllowedResponse writes an error message to the response and sets the Allow header.
func WriteMethodNotAllowedResponse(w http.ResponseWriter, allowedMethods []string) {
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(&errorResponse{Err: http.StatusText(http.StatusMethodNotAllowed)})
}

View file

@ -1,4 +1,4 @@
package http
package handler
import (
"github.com/portainer/portainer"
@ -10,6 +10,8 @@ import (
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
// AuthHandler represents an HTTP API handler for managing authentication.
@ -33,37 +35,38 @@ const (
)
// NewAuthHandler returns a new instance of AuthHandler.
func NewAuthHandler(mw *middleWareService) *AuthHandler {
func NewAuthHandler(bouncer *security.RequestBouncer, authDisabled bool) *AuthHandler {
h := &AuthHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
authDisabled: authDisabled,
}
h.Handle("/auth",
mw.public(http.HandlerFunc(h.handlePostAuth)))
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth)))
return h
}
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost})
return
}
if handler.authDisabled {
Error(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
httperror.WriteErrorResponse(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
var req postAuthRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
return
}
@ -72,16 +75,16 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
u, err := handler.UserService.UserByUsername(username)
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
Error(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
return
}
@ -92,7 +95,7 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
}
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
@ -100,7 +103,7 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
}
type postAuthRequest struct {
Username string `valid:"alphanum,required"`
Username string `valid:"required"`
Password string `valid:"required"`
}

View file

@ -0,0 +1,94 @@
package handler
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"
)
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
type DockerHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
TeamMembershipService portainer.TeamMembershipService
ProxyManager *proxy.Manager
}
// NewDockerHandler returns a new instance of DockerHandler.
func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
h := &DockerHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.PathPrefix("/{id}/").Handler(
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
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"]
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
}
if tokenData.Role != portainer.AdministratorRole && !handler.checkEndpointAccessControl(endpoint, tokenData.ID) {
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
}
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
}

View file

@ -1,7 +1,10 @@
package http
package handler
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"encoding/json"
"log"
@ -20,7 +23,7 @@ type EndpointHandler struct {
authorizeEndpointManagement bool
EndpointService portainer.EndpointService
FileService portainer.FileService
ProxyService *ProxyService
ProxyManager *proxy.Manager
}
const (
@ -30,78 +33,67 @@ const (
)
// NewEndpointHandler returns a new instance of EndpointHandler.
func NewEndpointHandler(mw *middleWareService) *EndpointHandler {
func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *EndpointHandler {
h := &EndpointHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
authorizeEndpointManagement: authorizeEndpointManagement,
}
h.Handle("/endpoints",
mw.administrator(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
h.Handle("/endpoints",
mw.authenticated(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
mw.administrator(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
mw.administrator(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}/access",
mw.administrator(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}",
mw.administrator(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
return h
}
// handleGetEndpoints handles GET requests on /endpoints
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
tokenData, err := extractTokenDataFromRequestContext(r)
filteredEndpoints, err := security.FilterEndpoints(endpoints, securityContext)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
if tokenData == nil {
Error(w, portainer.ErrInvalidJWTToken, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, 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)
encodeJSON(w, filteredEndpoints, handler.Logger)
}
// handlePostEndpoints handles POST requests on /endpoints
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
var req postEndpointsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
@ -111,11 +103,12 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
PublicURL: req.PublicURL,
TLS: req.TLS,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
@ -128,7 +121,7 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
endpoint.TLSKeyPath = keyPath
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
@ -154,16 +147,16 @@ func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http
endpointID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(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)
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
@ -177,52 +170,63 @@ func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r
endpointID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(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)
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(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)
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
authorizedUserIDs := []portainer.UserID{}
for _, value := range req.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
if req.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range req.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
endpoint.AuthorizedUsers = authorizedUserIDs
}
if req.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range req.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
endpoint.AuthorizedTeams = authorizedTeamIDs
}
endpoint.AuthorizedUsers = authorizedUserIDs
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putEndpointAccessRequest struct {
AuthorizedUsers []int `valid:"-"`
AuthorizedTeams []int `valid:"-"`
}
// handlePutEndpoint handles PUT requests on /endpoints/:id
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
@ -231,28 +235,28 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
endpointID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointsRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(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)
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
@ -283,20 +287,20 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
endpoint.TLSKeyPath = ""
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
_, err = handler.ProxyService.CreateAndRegisterProxy(endpoint)
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
@ -311,7 +315,7 @@ type putEndpointsRequest struct {
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
@ -320,32 +324,33 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
endpointID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(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)
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
handler.ProxyService.DeleteProxy(string(endpointID))
handler.ProxyManager.DeleteProxy(string(endpointID))
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if endpoint.TLS {
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
}

View file

@ -1,4 +1,4 @@
package http
package handler
import (
"net/http"
@ -10,7 +10,8 @@ type FileHandler struct {
http.Handler
}
func newFileHandler(assetPath string) *FileHandler {
// NewFileHandler returns a new instance of FileHandler.
func NewFileHandler(assetPath string) *FileHandler {
h := &FileHandler{
Handler: http.FileServer(http.Dir(assetPath)),
}

View file

@ -1,25 +1,29 @@
package http
package handler
import (
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"strings"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
)
// Handler is a collection of all the service handlers.
type Handler struct {
AuthHandler *AuthHandler
UserHandler *UserHandler
EndpointHandler *EndpointHandler
SettingsHandler *SettingsHandler
TemplatesHandler *TemplatesHandler
DockerHandler *DockerHandler
WebSocketHandler *WebSocketHandler
UploadHandler *UploadHandler
FileHandler *FileHandler
AuthHandler *AuthHandler
UserHandler *UserHandler
TeamHandler *TeamHandler
TeamMembershipHandler *TeamMembershipHandler
EndpointHandler *EndpointHandler
ResourceHandler *ResourceHandler
SettingsHandler *SettingsHandler
TemplatesHandler *TemplatesHandler
DockerHandler *DockerHandler
WebSocketHandler *WebSocketHandler
UploadHandler *UploadHandler
FileHandler *FileHandler
}
const (
@ -30,7 +34,7 @@ const (
// 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")
// ErrEmptyResponseBody = portainer.Error("Empty response body")
)
// ServeHTTP delegates a request to the appropriate subhandler.
@ -39,8 +43,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/teams") {
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/team_memberships") {
http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/endpoints") {
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/resource_controls") {
http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
@ -56,33 +66,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
// Error writes an API error message to the response and logger.
func Error(w http.ResponseWriter, err error, code int, logger *log.Logger) {
// Log error.
if logger != nil {
logger.Printf("http error: %s (code=%d)", err, code)
}
// Write generic error response.
w.WriteHeader(code)
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
}
// errorResponse is a generic response for sending a error.
type errorResponse struct {
Err string `json:"err,omitempty"`
}
// handleNotAllowed writes an API error message to the response and sets the Allow header.
func handleNotAllowed(w http.ResponseWriter, allowedMethods []string) {
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(&errorResponse{Err: http.StatusText(http.StatusMethodNotAllowed)})
}
// encodeJSON encodes v to w in JSON format. Error() is called if encoding fails.
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
if err := json.NewEncoder(w).Encode(v); err != nil {
Error(w, err, http.StatusInternalServerError, logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, logger)
}
}

View file

@ -0,0 +1,256 @@
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/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// ResourceHandler represents an HTTP API handler for managing resource controls.
type ResourceHandler struct {
*mux.Router
Logger *log.Logger
ResourceControlService portainer.ResourceControlService
}
// NewResourceHandler returns a new instance of ResourceHandler.
func NewResourceHandler(bouncer *security.RequestBouncer) *ResourceHandler {
h := &ResourceHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/resource_controls",
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostResources))).Methods(http.MethodPost)
h.Handle("/resource_controls/{id}",
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePutResources))).Methods(http.MethodPut)
h.Handle("/resource_controls/{id}",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleDeleteResources))).Methods(http.MethodDelete)
return h
}
// handlePostResources handles POST requests on /resources
func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *http.Request) {
var req postResourcesRequest
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
}
var resourceControlType portainer.ResourceControlType
switch req.Type {
case "container":
resourceControlType = portainer.ContainerResourceControl
case "service":
resourceControlType = portainer.ServiceResourceControl
case "volume":
resourceControlType = portainer.VolumeResourceControl
default:
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
return
}
if len(req.Users) == 0 && len(req.Teams) == 0 && !req.AdministratorsOnly {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
rc, err := handler.ResourceControlService.ResourceControlByResourceID(req.ResourceID)
if err != nil && err != portainer.ErrResourceControlNotFound {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if rc != nil {
httperror.WriteErrorResponse(w, portainer.ErrResourceControlAlreadyExists, http.StatusConflict, handler.Logger)
return
}
var userAccesses = make([]portainer.UserResourceAccess, 0)
for _, v := range req.Users {
userAccess := portainer.UserResourceAccess{
UserID: portainer.UserID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
userAccesses = append(userAccesses, userAccess)
}
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
for _, v := range req.Teams {
teamAccess := portainer.TeamResourceAccess{
TeamID: portainer.TeamID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
teamAccesses = append(teamAccesses, teamAccess)
}
resourceControl := portainer.ResourceControl{
ResourceID: req.ResourceID,
SubResourceIDs: req.SubResourceIDs,
Type: resourceControlType,
AdministratorsOnly: req.AdministratorsOnly,
UserAccesses: userAccesses,
TeamAccesses: teamAccesses,
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedResourceControlCreation(&resourceControl, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
return
}
type postResourcesRequest struct {
ResourceID string `valid:"required"`
Type string `valid:"required"`
AdministratorsOnly bool `valid:"-"`
Users []int `valid:"-"`
Teams []int `valid:"-"`
SubResourceIDs []string `valid:"-"`
}
// handlePutResources handles PUT requests on /resources/:id
func (handler *ResourceHandler) handlePutResources(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
resourceControlID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putResourcesRequest
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
}
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
if err == portainer.ErrResourceControlNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
resourceControl.AdministratorsOnly = req.AdministratorsOnly
var userAccesses = make([]portainer.UserResourceAccess, 0)
for _, v := range req.Users {
userAccess := portainer.UserResourceAccess{
UserID: portainer.UserID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
userAccesses = append(userAccesses, userAccess)
}
resourceControl.UserAccesses = userAccesses
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
for _, v := range req.Teams {
teamAccess := portainer.TeamResourceAccess{
TeamID: portainer.TeamID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
teamAccesses = append(teamAccesses, teamAccess)
}
resourceControl.TeamAccesses = teamAccesses
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedResourceControlUpdate(resourceControl, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
err = handler.ResourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putResourcesRequest struct {
AdministratorsOnly bool `valid:"-"`
Users []int `valid:"-"`
Teams []int `valid:"-"`
}
// handleDeleteResources handles DELETE requests on /resources/:id
func (handler *ResourceHandler) handleDeleteResources(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
resourceControlID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
if err == portainer.ErrResourceControlNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedResourceControlDeletion(resourceControl, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}

View file

@ -1,7 +1,9 @@
package http
package handler
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
@ -18,13 +20,14 @@ type SettingsHandler struct {
}
// NewSettingsHandler returns a new instance of SettingsHandler.
func NewSettingsHandler(mw *middleWareService) *SettingsHandler {
func NewSettingsHandler(bouncer *security.RequestBouncer, settings *portainer.Settings) *SettingsHandler {
h := &SettingsHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
settings: settings,
}
h.Handle("/settings",
mw.public(http.HandlerFunc(h.handleGetSettings)))
bouncer.PublicAccess(http.HandlerFunc(h.handleGetSettings)))
return h
}
@ -32,7 +35,7 @@ func NewSettingsHandler(mw *middleWareService) *SettingsHandler {
// handleGetSettings handles GET requests on /settings
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
handleNotAllowed(w, []string{http.MethodGet})
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet})
return
}

252
api/http/handler/team.go Normal file
View file

@ -0,0 +1,252 @@
package handler
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"encoding/json"
"log"
"net/http"
"os"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// TeamHandler represents an HTTP API handler for managing teams.
type TeamHandler struct {
*mux.Router
Logger *log.Logger
TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService
ResourceControlService portainer.ResourceControlService
}
// NewTeamHandler returns a new instance of TeamHandler.
func NewTeamHandler(bouncer *security.RequestBouncer) *TeamHandler {
h := &TeamHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/teams",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostTeams))).Methods(http.MethodPost)
h.Handle("/teams",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet)
h.Handle("/teams/{id}",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeam))).Methods(http.MethodGet)
h.Handle("/teams/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutTeam))).Methods(http.MethodPut)
h.Handle("/teams/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteTeam))).Methods(http.MethodDelete)
h.Handle("/teams/{id}/memberships",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetMemberships))).Methods(http.MethodGet)
return h
}
// handlePostTeams handles POST requests on /teams
func (handler *TeamHandler) handlePostTeams(w http.ResponseWriter, r *http.Request) {
var req postTeamsRequest
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
}
team, err := handler.TeamService.TeamByName(req.Name)
if err != nil && err != portainer.ErrTeamNotFound {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if team != nil {
httperror.WriteErrorResponse(w, portainer.ErrTeamAlreadyExists, http.StatusConflict, handler.Logger)
return
}
team = &portainer.Team{
Name: req.Name,
}
err = handler.TeamService.CreateTeam(team)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postTeamsResponse{ID: int(team.ID)}, handler.Logger)
}
type postTeamsResponse struct {
ID int `json:"Id"`
}
type postTeamsRequest struct {
Name string `valid:"required"`
}
// handleGetTeams handles GET requests on /teams
func (handler *TeamHandler) handleGetTeams(w http.ResponseWriter, r *http.Request) {
teams, err := handler.TeamService.Teams()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, teams, handler.Logger)
}
// handleGetTeam handles GET requests on /teams/:id
func (handler *TeamHandler) handleGetTeam(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
tid, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
teamID := portainer.TeamID(tid)
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedTeamManagement(teamID, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
team, err := handler.TeamService.Team(teamID)
if err == portainer.ErrTeamNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &team, handler.Logger)
}
// handlePutTeam handles PUT requests on /teams/:id
func (handler *TeamHandler) handlePutTeam(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
teamID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putTeamRequest
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
}
team, err := handler.TeamService.Team(portainer.TeamID(teamID))
if err == portainer.ErrTeamNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.Name != "" {
team.Name = req.Name
}
err = handler.TeamService.UpdateTeam(team.ID, team)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putTeamRequest struct {
Name string `valid:"-"`
}
// handleDeleteTeam handles DELETE requests on /teams/:id
func (handler *TeamHandler) handleDeleteTeam(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
teamID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
_, err = handler.TeamService.Team(portainer.TeamID(teamID))
if err == portainer.ErrTeamNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.TeamService.DeleteTeam(portainer.TeamID(teamID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.TeamMembershipService.DeleteTeamMembershipByTeamID(portainer.TeamID(teamID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handleGetMemberships handles GET requests on /teams/:id/memberships
func (handler *TeamHandler) handleGetMemberships(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
tid, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
teamID := portainer.TeamID(tid)
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedTeamManagement(teamID, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
memberships, err := handler.TeamMembershipService.TeamMembershipsByTeamID(teamID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, memberships, handler.Logger)
}

View file

@ -0,0 +1,240 @@
package handler
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"encoding/json"
"log"
"net/http"
"os"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// TeamMembershipHandler represents an HTTP API handler for managing teams.
type TeamMembershipHandler struct {
*mux.Router
Logger *log.Logger
TeamMembershipService portainer.TeamMembershipService
ResourceControlService portainer.ResourceControlService
}
// NewTeamMembershipHandler returns a new instance of TeamMembershipHandler.
func NewTeamMembershipHandler(bouncer *security.RequestBouncer) *TeamMembershipHandler {
h := &TeamMembershipHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/team_memberships",
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostTeamMemberships))).Methods(http.MethodPost)
h.Handle("/team_memberships",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeamsMemberships))).Methods(http.MethodGet)
h.Handle("/team_memberships/{id}",
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePutTeamMembership))).Methods(http.MethodPut)
h.Handle("/team_memberships/{id}",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleDeleteTeamMembership))).Methods(http.MethodDelete)
return h
}
// handlePostTeamMemberships handles POST requests on /team_memberships
func (handler *TeamMembershipHandler) handlePostTeamMemberships(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
var req postTeamMembershipsRequest
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
}
userID := portainer.UserID(req.UserID)
teamID := portainer.TeamID(req.TeamID)
role := portainer.MembershipRole(req.Role)
if !security.AuthorizedTeamManagement(teamID, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if len(memberships) > 0 {
for _, membership := range memberships {
if membership.UserID == userID && membership.TeamID == teamID {
httperror.WriteErrorResponse(w, portainer.ErrTeamMembershipAlreadyExists, http.StatusConflict, handler.Logger)
return
}
}
}
membership := &portainer.TeamMembership{
UserID: userID,
TeamID: teamID,
Role: role,
}
err = handler.TeamMembershipService.CreateTeamMembership(membership)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postTeamMembershipsResponse{ID: int(membership.ID)}, handler.Logger)
}
type postTeamMembershipsResponse struct {
ID int `json:"Id"`
}
type postTeamMembershipsRequest struct {
UserID int `valid:"required"`
TeamID int `valid:"required"`
Role int `valid:"required"`
}
// handleGetTeamsMemberships handles GET requests on /team_memberships
func (handler *TeamMembershipHandler) handleGetTeamsMemberships(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
memberships, err := handler.TeamMembershipService.TeamMemberships()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, memberships, handler.Logger)
}
// handlePutTeamMembership handles PUT requests on /team_memberships/:id
func (handler *TeamMembershipHandler) handlePutTeamMembership(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
membershipID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putTeamMembershipRequest
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
}
userID := portainer.UserID(req.UserID)
teamID := portainer.TeamID(req.TeamID)
role := portainer.MembershipRole(req.Role)
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedTeamManagement(teamID, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID))
if err == portainer.ErrTeamMembershipNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if securityContext.IsTeamLeader && membership.Role != role {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
membership.UserID = userID
membership.TeamID = teamID
membership.Role = role
err = handler.TeamMembershipService.UpdateTeamMembership(membership.ID, membership)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putTeamMembershipRequest struct {
UserID int `valid:"required"`
TeamID int `valid:"required"`
Role int `valid:"required"`
}
// handleDeleteTeamMembership handles DELETE requests on /team_memberships/:id
func (handler *TeamMembershipHandler) handleDeleteTeamMembership(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
membershipID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID))
if err == portainer.ErrTeamMembershipNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedTeamManagement(membership.TeamID, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
err = handler.TeamMembershipService.DeleteTeamMembership(portainer.TeamMembershipID(membershipID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}

View file

@ -1,4 +1,4 @@
package http
package handler
import (
"io/ioutil"
@ -7,6 +7,8 @@ import (
"os"
"github.com/gorilla/mux"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
// TemplatesHandler represents an HTTP API handler for managing templates.
@ -21,26 +23,27 @@ const (
)
// NewTemplatesHandler returns a new instance of TemplatesHandler.
func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
func NewTemplatesHandler(bouncer *security.RequestBouncer, containerTemplatesURL string) *TemplatesHandler {
h := &TemplatesHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
containerTemplatesURL: containerTemplatesURL,
}
h.Handle("/templates",
mw.authenticated(http.HandlerFunc(h.handleGetTemplates)))
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates)))
return h
}
// handleGetTemplates handles GET requests on /templates?key=<key>
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
handleNotAllowed(w, []string{http.MethodGet})
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet})
return
}
key := r.FormValue("key")
if key == "" {
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
return
}
@ -50,19 +53,19 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht
} else if key == "linuxserver.io" {
templatesURL = containerTemplatesURLLinuxServerIo
} else {
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
return
}
resp, err := http.Get(templatesURL)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
w.Header().Set("Content-Type", "application/json")

View file

@ -1,7 +1,9 @@
package http
package handler
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
@ -19,19 +21,19 @@ type UploadHandler struct {
}
// NewUploadHandler returns a new instance of UploadHandler.
func NewUploadHandler(mw *middleWareService) *UploadHandler {
func NewUploadHandler(bouncer *security.RequestBouncer) *UploadHandler {
h := &UploadHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}",
mw.authenticated(http.HandlerFunc(h.handlePostUploadTLS)))
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUploadTLS)))
return h
}
func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost})
return
}
@ -40,14 +42,14 @@ func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http
certificate := vars["certificate"]
ID, err := strconv.Atoi(endpointID)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
file, _, err := r.FormFile("file")
defer file.Close()
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
@ -60,12 +62,13 @@ func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http
case "key":
fileType = portainer.TLSFileKey
default:
Error(w, portainer.ErrUndefinedTLSFileType, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, portainer.ErrUndefinedTLSFileType, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.FileService.StoreTLSFile(portainer.EndpointID(ID), fileType, file)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}

490
api/http/handler/user.go Normal file
View file

@ -0,0 +1,490 @@
package handler
import (
"strconv"
"strings"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"encoding/json"
"log"
"net/http"
"os"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// UserHandler represents an HTTP API handler for managing users.
type UserHandler struct {
*mux.Router
Logger *log.Logger
UserService portainer.UserService
TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService
ResourceControlService portainer.ResourceControlService
CryptoService portainer.CryptoService
}
// NewUserHandler returns a new instance of UserHandler.
func NewUserHandler(bouncer *security.RequestBouncer) *UserHandler {
h := &UserHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/users",
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
h.Handle("/users",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
h.Handle("/users/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
h.Handle("/users/{id}",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
h.Handle("/users/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
h.Handle("/users/{id}/memberships",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetMemberships))).Methods(http.MethodGet)
h.Handle("/users/{id}/teams",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet)
h.Handle("/users/{id}/passwd",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUserPasswd)))
h.Handle("/users/admin/check",
bouncer.PublicAccess(http.HandlerFunc(h.handleGetAdminCheck)))
h.Handle("/users/admin/init",
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAdminInit)))
return h
}
// handlePostUsers handles POST requests on /users
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
var req postUsersRequest
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
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
return
}
if securityContext.IsTeamLeader && req.Role == 1 {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
return
}
if strings.ContainsAny(req.Username, " ") {
httperror.WriteErrorResponse(w, portainer.ErrInvalidUsername, http.StatusBadRequest, handler.Logger)
return
}
var role portainer.UserRole
if req.Role == 1 {
role = portainer.AdministratorRole
} else {
role = portainer.StandardUserRole
}
user, err := handler.UserService.UserByUsername(req.Username)
if err != nil && err != portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if user != nil {
httperror.WriteErrorResponse(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 {
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.CreateUser(user)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postUsersResponse{ID: int(user.ID)}, handler.Logger)
}
type postUsersResponse struct {
ID int `json:"Id"`
}
type postUsersRequest struct {
Username string `valid:"required"`
Password string `valid:"required"`
Role int `valid:"required"`
}
// handleGetUsers handles GET requests on /users
func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
users, err := handler.UserService.Users()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
filteredUsers := security.FilterUsers(users, securityContext)
for i := range filteredUsers {
filteredUsers[i].Password = ""
}
encodeJSON(w, filteredUsers, 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 {
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost})
return
}
vars := mux.Vars(r)
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req postUserPasswdRequest
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
}
var password = req.Password
u, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
valid := true
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
valid = false
}
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
}
type postUserPasswdRequest struct {
Password string `valid:"required"`
}
type postUserPasswdResponse struct {
Valid bool `json:"valid"`
}
// handleGetUser handles GET requests on /users/:id
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
user, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
user.Password = ""
encodeJSON(w, &user, handler.Logger)
}
// 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 {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
return
}
var req putUserRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
if req.Password == "" && req.Role == 0 {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
user, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.Password != "" {
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
}
if req.Role != 0 {
if tokenData.Role != portainer.AdministratorRole {
httperror.WriteErrorResponse(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 {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putUserRequest struct {
Password string `valid:"-"`
Role int `valid:"-"`
}
// handlePostAdminInit handles GET requests on /users/admin/check
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet})
return
}
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if len(users) == 0 {
httperror.WriteErrorResponse(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
return
}
}
// handlePostAdminInit handles POST requests on /users/admin/init
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost})
return
}
var req postAdminInitRequest
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
}
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 {
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.CreateUser(user)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if user != nil {
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger)
return
}
}
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 {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
_, err = handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.UserService.DeleteUser(portainer.UserID(userID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.TeamMembershipService.DeleteTeamMembershipByUserID(portainer.UserID(userID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handleGetMemberships handles GET requests on /users/:id/memberships
func (handler *UserHandler) handleGetMemberships(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
return
}
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(portainer.UserID(userID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, memberships, handler.Logger)
}
// handleGetTeams handles GET requests on /users/:id/teams
func (handler *UserHandler) handleGetTeams(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
uid, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
userID := portainer.UserID(uid)
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedUserManagement(userID, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
teams, err := handler.TeamService.Teams()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
filteredTeams := security.FilterUserTeams(teams, securityContext)
encodeJSON(w, filteredTeams, handler.Logger)
}

View file

@ -1,4 +1,4 @@
package http
package handler
import (
"bytes"
@ -17,6 +17,7 @@ import (
"github.com/gorilla/mux"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
"golang.org/x/net/websocket"
)
@ -71,7 +72,7 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
// Should not be managed here
var tlsConfig *tls.Config
if endpoint.TLS {
tlsConfig, err = createTLSConfiguration(endpoint.TLSCACertPath,
tlsConfig, err = crypto.CreateTLSConfiguration(endpoint.TLSCACertPath,
endpoint.TLSCertPath,
endpoint.TLSKeyPath)
if err != nil {

View file

@ -1,119 +0,0 @@
package http
import (
"context"
"github.com/portainer/portainer"
"net/http"
"strings"
)
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
}
// 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
}
// 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")
next.ServeHTTP(w, r)
})
}
// 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
// Get token from the Authorization header
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
Error(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
return
}
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,
}
}
ctx := context.WithValue(r.Context(), contextAuthenticationKey, tokenData)
next.ServeHTTP(w, r.WithContext(ctx))
return
})
}

View file

@ -1,67 +0,0 @@
package http
import (
"net/http"
"net/url"
"github.com/orcaman/concurrent-map"
"github.com/portainer/portainer"
)
// ProxyService represents a service used to manage Docker proxies.
type ProxyService struct {
proxyFactory *ProxyFactory
proxies cmap.ConcurrentMap
}
// NewProxyService initializes a new ProxyService
func NewProxyService(resourceControlService portainer.ResourceControlService) *ProxyService {
return &ProxyService{
proxies: cmap.New(),
proxyFactory: &ProxyFactory{
ResourceControlService: resourceControlService,
},
}
}
// CreateAndRegisterProxy creates a new HTTP reverse proxy and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (service *ProxyService) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
var proxy http.Handler
endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return nil, err
}
if endpointURL.Scheme == "tcp" {
if endpoint.TLS {
proxy, err = service.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
if err != nil {
return nil, err
}
} else {
proxy = service.proxyFactory.newHTTPProxy(endpointURL)
}
} else {
// Assume unix:// scheme
proxy = service.proxyFactory.newSocketProxy(endpointURL.Path)
}
service.proxies.Set(string(endpoint.ID), proxy)
return proxy, nil
}
// GetProxy returns the proxy associated to a key
func (service *ProxyService) GetProxy(key string) http.Handler {
proxy, ok := service.proxies.Get(key)
if !ok {
return nil
}
return proxy.(http.Handler)
}
// DeleteProxy deletes the proxy associated to a key
func (service *ProxyService) DeleteProxy(key string) {
service.proxies.Remove(key)
}

View file

@ -0,0 +1,21 @@
package proxy
import "github.com/portainer/portainer"
func canUserAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool {
for _, authorizedUserAccess := range resourceControl.UserAccesses {
if userID == authorizedUserAccess.UserID {
return true
}
}
for _, authorizedTeamAccess := range resourceControl.TeamAccesses {
for _, userTeamID := range userTeamIDs {
if userTeamID == authorizedTeamAccess.TeamID {
return true
}
}
}
return false
}

View file

@ -0,0 +1,98 @@
package proxy
import (
"net/http"
"github.com/portainer/portainer"
)
const (
// ErrDockerContainerIdentifierNotFound defines an error raised when Portainer is unable to find a container identifier
ErrDockerContainerIdentifierNotFound = portainer.Error("Docker container identifier not found")
containerIdentifier = "Id"
containerLabelForServiceIdentifier = "com.docker.swarm.service.id"
)
// containerListOperation extracts the response as a JSON object, loop through the containers array
// decorate and/or filter the containers based on resource controls before rewriting the response
func containerListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
var err error
// ContainerList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
responseArray, err := getResponseAsJSONArray(response)
if err != nil {
return err
}
if operationContext.isAdmin {
responseArray, err = decorateContainerList(responseArray, operationContext.resourceControls)
} else {
responseArray, err = filterContainerList(responseArray, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
}
if err != nil {
return err
}
return rewriteResponse(response, responseArray, http.StatusOK)
}
// containerInspectOperation extracts the response as a JSON object, verify that the user
// has access to the container based on resource control (check are done based on the containerID and optional Swarm service ID)
// and either rewrite an access denied response or a decorated container.
func containerInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
// ContainerInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
responseObject, err := getResponseAsJSONOBject(response)
if err != nil {
return err
}
if responseObject[containerIdentifier] == nil {
return ErrDockerContainerIdentifierNotFound
}
containerID := responseObject[containerIdentifier].(string)
resourceControl := getResourceControlByResourceID(containerID, operationContext.resourceControls)
if resourceControl != nil {
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
}
}
containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject)
if containerLabels != nil && containerLabels[containerLabelForServiceIdentifier] != nil {
serviceID := containerLabels[containerLabelForServiceIdentifier].(string)
resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls)
if resourceControl != nil {
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
}
}
}
return rewriteResponse(response, responseObject, http.StatusOK)
}
// extractContainerLabelsFromContainerInspectObject retrieve the Labels of the container if present.
// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
func extractContainerLabelsFromContainerInspectObject(responseObject map[string]interface{}) map[string]interface{} {
// Labels are stored under Config.Labels
containerConfigObject := extractJSONField(responseObject, "Config")
if containerConfigObject != nil {
containerLabelsObject := extractJSONField(containerConfigObject, "Labels")
return containerLabelsObject
}
return nil
}
// extractContainerLabelsFromContainerListObject retrieve the Labels of the container if present.
// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
func extractContainerLabelsFromContainerListObject(responseObject map[string]interface{}) map[string]interface{} {
// Labels are stored under Labels
containerLabelsObject := extractJSONField(responseObject, "Labels")
return containerLabelsObject
}

View file

@ -0,0 +1,90 @@
package proxy
import "github.com/portainer/portainer"
// decorateVolumeList loops through all volumes and will decorate any volume with an existing resource control.
// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
func decorateVolumeList(volumeData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
decoratedVolumeData := make([]interface{}, 0)
for _, volume := range volumeData {
volumeObject := volume.(map[string]interface{})
if volumeObject[volumeIdentifier] == nil {
return nil, ErrDockerVolumeIdentifierNotFound
}
volumeID := volumeObject[volumeIdentifier].(string)
resourceControl := getResourceControlByResourceID(volumeID, resourceControls)
if resourceControl != nil {
volumeObject = decorateObject(volumeObject, resourceControl)
}
decoratedVolumeData = append(decoratedVolumeData, volumeObject)
}
return decoratedVolumeData, nil
}
// decorateContainerList loops through all containers and will decorate any container with an existing resource control.
// Check is based on the container ID and optional Swarm service ID.
// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
func decorateContainerList(containerData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
decoratedContainerData := make([]interface{}, 0)
for _, container := range containerData {
containerObject := container.(map[string]interface{})
if containerObject[containerIdentifier] == nil {
return nil, ErrDockerContainerIdentifierNotFound
}
containerID := containerObject[containerIdentifier].(string)
resourceControl := getResourceControlByResourceID(containerID, resourceControls)
if resourceControl != nil {
containerObject = decorateObject(containerObject, resourceControl)
}
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
if containerLabels != nil && containerLabels[containerLabelForServiceIdentifier] != nil {
serviceID := containerLabels[containerLabelForServiceIdentifier].(string)
resourceControl := getResourceControlByResourceID(serviceID, resourceControls)
if resourceControl != nil {
containerObject = decorateObject(containerObject, resourceControl)
}
}
decoratedContainerData = append(decoratedContainerData, containerObject)
}
return decoratedContainerData, nil
}
// decorateServiceList loops through all services and will decorate any service with an existing resource control.
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
func decorateServiceList(serviceData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
decoratedServiceData := make([]interface{}, 0)
for _, service := range serviceData {
serviceObject := service.(map[string]interface{})
if serviceObject[serviceIdentifier] == nil {
return nil, ErrDockerServiceIdentifierNotFound
}
serviceID := serviceObject[serviceIdentifier].(string)
resourceControl := getResourceControlByResourceID(serviceID, resourceControls)
if resourceControl != nil {
serviceObject = decorateObject(serviceObject, resourceControl)
}
decoratedServiceData = append(decoratedServiceData, serviceObject)
}
return decoratedServiceData, nil
}
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
metadata := make(map[string]interface{})
metadata["ResourceControl"] = resourceControl
object["Portainer"] = metadata
return object
}

55
api/http/proxy/factory.go Normal file
View file

@ -0,0 +1,55 @@
package proxy
import (
"net/http"
"net/http/httputil"
"net/url"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
)
// proxyFactory is a factory to create reverse proxies to Docker endpoints
type proxyFactory struct {
ResourceControlService portainer.ResourceControlService
TeamMembershipService portainer.TeamMembershipService
}
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
u.Scheme = "http"
return factory.createReverseProxy(u)
}
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
u.Scheme = "https"
proxy := factory.createReverseProxy(u)
config, err := crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
if err != nil {
return nil, err
}
proxy.Transport.(*proxyTransport).dockerTransport.TLSClientConfig = config
return proxy, nil
}
func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
proxy := &socketProxy{}
transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService,
TeamMembershipService: factory.TeamMembershipService,
dockerTransport: newSocketTransport(path),
}
proxy.Transport = transport
return proxy
}
func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReverseProxy {
proxy := newSingleHostReverseProxyWithHostHeader(u)
transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService,
TeamMembershipService: factory.TeamMembershipService,
dockerTransport: newHTTPTransport(),
}
proxy.Transport = transport
return proxy
}

91
api/http/proxy/filter.go Normal file
View file

@ -0,0 +1,91 @@
package proxy
import "github.com/portainer/portainer"
// filterVolumeList loops through all volumes, filters volumes without any resource control (public resources) or with
// any resource control giving access to the user (these volumes will be decorated).
// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
func filterVolumeList(volumeData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
filteredVolumeData := make([]interface{}, 0)
for _, volume := range volumeData {
volumeObject := volume.(map[string]interface{})
if volumeObject[volumeIdentifier] == nil {
return nil, ErrDockerVolumeIdentifierNotFound
}
volumeID := volumeObject[volumeIdentifier].(string)
resourceControl := getResourceControlByResourceID(volumeID, resourceControls)
if resourceControl == nil {
filteredVolumeData = append(filteredVolumeData, volumeObject)
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
volumeObject = decorateObject(volumeObject, resourceControl)
filteredVolumeData = append(filteredVolumeData, volumeObject)
}
}
return filteredVolumeData, nil
}
// filterContainerList loops through all containers, filters containers without any resource control (public resources) or with
// any resource control giving access to the user (check on container ID and optional Swarm service ID, these containers will be decorated).
// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
func filterContainerList(containerData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
filteredContainerData := make([]interface{}, 0)
for _, container := range containerData {
containerObject := container.(map[string]interface{})
if containerObject[containerIdentifier] == nil {
return nil, ErrDockerContainerIdentifierNotFound
}
containerID := containerObject[containerIdentifier].(string)
resourceControl := getResourceControlByResourceID(containerID, resourceControls)
if resourceControl == nil {
// check if container is part of a Swarm service
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
if containerLabels != nil && containerLabels[containerLabelForServiceIdentifier] != nil {
serviceID := containerLabels[containerLabelForServiceIdentifier].(string)
serviceResourceControl := getResourceControlByResourceID(serviceID, resourceControls)
if serviceResourceControl == nil {
filteredContainerData = append(filteredContainerData, containerObject)
} else if serviceResourceControl != nil && canUserAccessResource(userID, userTeamIDs, serviceResourceControl) {
containerObject = decorateObject(containerObject, serviceResourceControl)
filteredContainerData = append(filteredContainerData, containerObject)
}
} else {
filteredContainerData = append(filteredContainerData, containerObject)
}
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
containerObject = decorateObject(containerObject, resourceControl)
filteredContainerData = append(filteredContainerData, containerObject)
}
}
return filteredContainerData, nil
}
// filterServiceList loops through all services, filters services without any resource control (public resources) or with
// any resource control giving access to the user (these services will be decorated).
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
func filterServiceList(serviceData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
filteredServiceData := make([]interface{}, 0)
for _, service := range serviceData {
serviceObject := service.(map[string]interface{})
if serviceObject[serviceIdentifier] == nil {
return nil, ErrDockerServiceIdentifierNotFound
}
serviceID := serviceObject[serviceIdentifier].(string)
resourceControl := getResourceControlByResourceID(serviceID, resourceControls)
if resourceControl == nil {
filteredServiceData = append(filteredServiceData, serviceObject)
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
serviceObject = decorateObject(serviceObject, resourceControl)
filteredServiceData = append(filteredServiceData, serviceObject)
}
}
return filteredServiceData, nil
}

68
api/http/proxy/manager.go Normal file
View file

@ -0,0 +1,68 @@
package proxy
import (
"net/http"
"net/url"
"github.com/orcaman/concurrent-map"
"github.com/portainer/portainer"
)
// Manager represents a service used to manage Docker proxies.
type Manager struct {
proxyFactory *proxyFactory
proxies cmap.ConcurrentMap
}
// NewManager initializes a new proxy Service
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService) *Manager {
return &Manager{
proxies: cmap.New(),
proxyFactory: &proxyFactory{
ResourceControlService: resourceControlService,
TeamMembershipService: teamMembershipService,
},
}
}
// CreateAndRegisterProxy creates a new HTTP reverse proxy and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
var proxy http.Handler
endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return nil, err
}
if endpointURL.Scheme == "tcp" {
if endpoint.TLS {
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
if err != nil {
return nil, err
}
} else {
proxy = manager.proxyFactory.newHTTPProxy(endpointURL)
}
} else {
// Assume unix:// scheme
proxy = manager.proxyFactory.newSocketProxy(endpointURL.Path)
}
manager.proxies.Set(string(endpoint.ID), proxy)
return proxy, nil
}
// GetProxy returns the proxy associated to a key
func (manager *Manager) GetProxy(key string) http.Handler {
proxy, ok := manager.proxies.Get(key)
if !ok {
return nil
}
return proxy.(http.Handler)
}
// DeleteProxy deletes the proxy associated to a key
func (manager *Manager) DeleteProxy(key string) {
manager.proxies.Remove(key)
}

View file

@ -0,0 +1,90 @@
package proxy
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"github.com/portainer/portainer"
)
const (
// 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")
)
func extractJSONField(jsonObject map[string]interface{}, key string) map[string]interface{} {
object := jsonObject[key]
if object != nil {
return object.(map[string]interface{})
}
return nil
}
func getResponseAsJSONOBject(response *http.Response) (map[string]interface{}, error) {
responseData, err := getResponseBodyAsGenericJSON(response)
if err != nil {
return nil, err
}
responseObject := responseData.(map[string]interface{})
return responseObject, nil
}
func getResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
responseData, err := getResponseBodyAsGenericJSON(response)
if err != nil {
return nil, err
}
responseObject := responseData.([]interface{})
return responseObject, nil
}
func getResponseBodyAsGenericJSON(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() (*http.Response, error) {
response := &http.Response{}
err := rewriteResponse(response, portainer.ErrResourceAccessDenied, http.StatusForbidden)
return response, err
}
func rewriteAccessDeniedResponse(response *http.Response) error {
return rewriteResponse(response, portainer.ErrResourceAccessDenied, http.StatusForbidden)
}
func rewriteResponse(response *http.Response, newResponseData interface{}, statusCode int) error {
jsonData, err := json.Marshal(newResponseData)
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
}

View file

@ -0,0 +1,46 @@
package proxy
import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
// 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", "")
}
}
return &httputil.ReverseProxy{Director: director}
}
// 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
}

64
api/http/proxy/service.go Normal file
View file

@ -0,0 +1,64 @@
package proxy
import (
"net/http"
"github.com/portainer/portainer"
)
const (
// ErrDockerServiceIdentifierNotFound defines an error raised when Portainer is unable to find a service identifier
ErrDockerServiceIdentifierNotFound = portainer.Error("Docker service identifier not found")
serviceIdentifier = "ID"
)
// serviceListOperation extracts the response as a JSON array, loop through the service array
// decorate and/or filter the services based on resource controls before rewriting the response
func serviceListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
var err error
// ServiceList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
responseArray, err := getResponseAsJSONArray(response)
if err != nil {
return err
}
if operationContext.isAdmin {
responseArray, err = decorateServiceList(responseArray, operationContext.resourceControls)
} else {
responseArray, err = filterServiceList(responseArray, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
}
if err != nil {
return err
}
return rewriteResponse(response, responseArray, http.StatusOK)
}
// serviceInspectOperation extracts the response as a JSON object, verify that the user
// has access to the service based on resource control and either rewrite an access denied response
// or a decorated service.
func serviceInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
// ServiceInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
responseObject, err := getResponseAsJSONOBject(response)
if err != nil {
return err
}
if responseObject[serviceIdentifier] == nil {
return ErrDockerServiceIdentifierNotFound
}
serviceID := responseObject[serviceIdentifier].(string)
resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls)
if resourceControl != nil {
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
}
}
return rewriteResponse(response, responseObject, http.StatusOK)
}

40
api/http/proxy/socket.go Normal file
View file

@ -0,0 +1,40 @@
package proxy
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
import (
"io"
"net/http"
httperror "github.com/portainer/portainer/http/error"
)
type socketProxy struct {
Transport *proxyTransport
}
func (proxy *socketProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Force URL/domain to http/unixsocket to be able to
// use http.Transport RoundTrip to do the requests via the socket
r.URL.Scheme = "http"
r.URL.Host = "unixsocket"
res, err := proxy.Transport.proxyDockerRequest(r)
if err != nil {
code := http.StatusInternalServerError
if res != nil && res.StatusCode != 0 {
code = res.StatusCode
}
httperror.WriteErrorResponse(w, err, code, 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 {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
}
}

237
api/http/proxy/transport.go Normal file
View file

@ -0,0 +1,237 @@
package proxy
import (
"net"
"net/http"
"path"
"strings"
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/security"
)
type (
proxyTransport struct {
dockerTransport *http.Transport
ResourceControlService portainer.ResourceControlService
TeamMembershipService portainer.TeamMembershipService
}
restrictedOperationContext struct {
isAdmin bool
userID portainer.UserID
userTeamIDs []portainer.TeamID
resourceControls []portainer.ResourceControl
}
restrictedOperationRequest func(*http.Request, *http.Response, *restrictedOperationContext) error
)
func newSocketTransport(socketPath string) *http.Transport {
return &http.Transport{
Dial: func(proto, addr string) (conn net.Conn, err error) {
return net.Dial("unix", socketPath)
},
}
}
func newHTTPTransport() *http.Transport {
return &http.Transport{}
}
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
return p.proxyDockerRequest(request)
}
func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) {
return p.dockerTransport.RoundTrip(request)
}
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
path := request.URL.Path
if strings.HasPrefix(path, "/containers") {
return p.proxyContainerRequest(request)
} else if strings.HasPrefix(path, "/services") {
return p.proxyServiceRequest(request)
} else if strings.HasPrefix(path, "/volumes") {
return p.proxyVolumeRequest(request)
}
return p.executeDockerRequest(request)
}
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
// return p.executeDockerRequest(request)
switch requestPath := request.URL.Path; requestPath {
case "/containers/create":
return p.executeDockerRequest(request)
case "/containers/prune":
return p.administratorOperation(request)
case "/containers/json":
return p.rewriteOperation(request, containerListOperation)
default:
// This section assumes /containers/**
if match, _ := path.Match("/containers/*/*", requestPath); match {
// Handle /containers/{id}/{action} requests
containerID := path.Base(path.Dir(requestPath))
action := path.Base(requestPath)
if action == "json" {
return p.rewriteOperation(request, containerInspectOperation)
}
return p.restrictedOperation(request, containerID)
} else if match, _ := path.Match("/containers/*", requestPath); match {
// Handle /containers/{id} requests
containerID := path.Base(requestPath)
return p.restrictedOperation(request, containerID)
}
return p.executeDockerRequest(request)
}
}
func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/services/create":
return p.executeDockerRequest(request)
case "/volumes/prune":
return p.administratorOperation(request)
case "/services":
return p.rewriteOperation(request, serviceListOperation)
default:
// This section assumes /services/**
if match, _ := path.Match("/services/*/*", requestPath); match {
// Handle /services/{id}/{action} requests
serviceID := path.Base(path.Dir(requestPath))
return p.restrictedOperation(request, serviceID)
} else if match, _ := path.Match("/services/*", requestPath); match {
// Handle /services/{id} requests
serviceID := path.Base(requestPath)
if request.Method == http.MethodGet {
return p.rewriteOperation(request, serviceInspectOperation)
}
return p.restrictedOperation(request, serviceID)
}
return p.executeDockerRequest(request)
}
}
func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/volumes/create":
return p.executeDockerRequest(request)
case "/volumes/prune":
return p.administratorOperation(request)
case "/volumes":
return p.rewriteOperation(request, volumeListOperation)
default:
// assume /volumes/{name}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, volumeInspectOperation)
}
volumeID := path.Base(requestPath)
return p.restrictedOperation(request, volumeID)
}
}
// restrictedOperation ensures that the current user has the required authorizations
// before executing the original request.
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
var err error
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
if tokenData.Role != portainer.AdministratorRole {
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
return nil, err
}
userTeamIDs := make([]portainer.TeamID, 0)
for _, membership := range teamMemberships {
userTeamIDs = append(userTeamIDs, membership.TeamID)
}
resourceControls, err := p.ResourceControlService.ResourceControls()
if err != nil {
return nil, err
}
resourceControl := getResourceControlByResourceID(resourceID, resourceControls)
if resourceControl != nil && !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
return writeAccessDeniedResponse()
}
}
return p.executeDockerRequest(request)
}
// rewriteOperation will create a new operation context with data that will be used
// to decorate the original request's response.
func (p *proxyTransport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
var err error
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
resourceControls, err := p.ResourceControlService.ResourceControls()
if err != nil {
return nil, err
}
operationContext := &restrictedOperationContext{
isAdmin: true,
userID: tokenData.ID,
resourceControls: resourceControls,
}
if tokenData.Role != portainer.AdministratorRole {
operationContext.isAdmin = false
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
return nil, err
}
userTeamIDs := make([]portainer.TeamID, 0)
for _, membership := range teamMemberships {
userTeamIDs = append(userTeamIDs, membership.TeamID)
}
operationContext.userTeamIDs = userTeamIDs
}
response, err := p.executeDockerRequest(request)
if err != nil {
return response, err
}
err = operation(request, response, operationContext)
return response, err
}
// administratorOperation ensures that the user has administrator privileges
// before executing the original request.
func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
if tokenData.Role != portainer.AdministratorRole {
return writeAccessDeniedResponse()
}
return p.executeDockerRequest(request)
}

17
api/http/proxy/utils.go Normal file
View file

@ -0,0 +1,17 @@
package proxy
import "github.com/portainer/portainer"
func getResourceControlByResourceID(resourceID string, resourceControls []portainer.ResourceControl) *portainer.ResourceControl {
for _, resourceControl := range resourceControls {
if resourceID == resourceControl.ResourceID {
return &resourceControl
}
for _, subResourceID := range resourceControl.SubResourceIDs {
if resourceID == subResourceID {
return &resourceControl
}
}
}
return nil
}

73
api/http/proxy/volumes.go Normal file
View file

@ -0,0 +1,73 @@
package proxy
import (
"net/http"
"github.com/portainer/portainer"
)
const (
// ErrDockerVolumeIdentifierNotFound defines an error raised when Portainer is unable to find a volume identifier
ErrDockerVolumeIdentifierNotFound = portainer.Error("Docker volume identifier not found")
volumeIdentifier = "Name"
)
// volumeListOperation extracts the response as a JSON object, loop through the volume array
// decorate and/or filter the volumes based on resource controls before rewriting the response
func volumeListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
var err error
// VolumeList response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
responseObject, err := getResponseAsJSONOBject(response)
if err != nil {
return err
}
// The "Volumes" field contains the list of volumes as an array of JSON objects
// Response schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
if responseObject["Volumes"] != nil {
volumeData := responseObject["Volumes"].([]interface{})
if operationContext.isAdmin {
volumeData, err = decorateVolumeList(volumeData, operationContext.resourceControls)
} else {
volumeData, err = filterVolumeList(volumeData, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
}
if err != nil {
return err
}
// Overwrite the original volume list
responseObject["Volumes"] = volumeData
}
return rewriteResponse(response, responseObject, http.StatusOK)
}
// volumeInspectOperation extracts the response as a JSON object, verify that the user
// has access to the volume based on resource control and either rewrite an access denied response
// or a decorated volume.
func volumeInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
// VolumeInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
responseObject, err := getResponseAsJSONOBject(response)
if err != nil {
return err
}
if responseObject[volumeIdentifier] == nil {
return ErrDockerVolumeIdentifierNotFound
}
volumeID := responseObject[volumeIdentifier].(string)
resourceControl := getResourceControlByResourceID(volumeID, operationContext.resourceControls)
if resourceControl != nil {
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
}
}
return rewriteResponse(response, responseObject, http.StatusOK)
}

View file

@ -1,664 +0,0 @@
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
}

View file

@ -0,0 +1,123 @@
package security
import "github.com/portainer/portainer"
// AuthorizedResourceControlDeletion ensure that the user can delete a resource control object.
// A non-administrator user cannot delete a resource control where:
// * the AdministratorsOnly flag is set
// * he is not one of the users in the user accesses
// * he is not a member of any team within the team accesses
func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
if context.IsAdmin {
return true
}
if resourceControl.AdministratorsOnly {
return false
}
userAccessesCount := len(resourceControl.UserAccesses)
teamAccessesCount := len(resourceControl.TeamAccesses)
if teamAccessesCount > 0 {
for _, access := range resourceControl.TeamAccesses {
for _, membership := range context.UserMemberships {
if membership.TeamID == access.TeamID && membership.Role == portainer.TeamLeader {
return true
}
}
}
}
if userAccessesCount > 0 {
for _, access := range resourceControl.UserAccesses {
if access.UserID == context.UserID {
return true
}
}
}
return false
}
// AuthorizedResourceControlUpdate ensure that the user can update a resource control object.
// It reuses the creation restrictions and adds extra checks.
// A non-administrator user cannot update a resource control where:
// * he wants to put one or more user in the user accesses
func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
userAccessesCount := len(resourceControl.UserAccesses)
if !context.IsAdmin && userAccessesCount > 0 {
return false
}
return AuthorizedResourceControlCreation(resourceControl, context)
}
// AuthorizedResourceControlCreation ensure that the user can create a resource control object.
// A non-administrator user cannot create a resource control where:
// * the AdministratorsOnly flag is set
// * he wants to add more than one user in the user accesses
// * he wants to add a team he is not a member of
func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
if context.IsAdmin {
return true
}
if resourceControl.AdministratorsOnly {
return false
}
userAccessesCount := len(resourceControl.UserAccesses)
teamAccessesCount := len(resourceControl.TeamAccesses)
if userAccessesCount > 1 || (userAccessesCount == 1 && teamAccessesCount == 1) {
return false
}
if userAccessesCount == 1 {
access := resourceControl.UserAccesses[0]
if access.UserID == context.UserID {
return true
}
}
if teamAccessesCount > 0 {
for _, access := range resourceControl.TeamAccesses {
isMember := false
for _, membership := range context.UserMemberships {
if membership.TeamID == access.TeamID {
isMember = true
}
}
if !isMember {
return false
}
}
}
return true
}
// AuthorizedTeamManagement ensure that access to the management of the specified team is granted.
// It will check if the user is either administrator or leader of that team.
func AuthorizedTeamManagement(teamID portainer.TeamID, context *RestrictedRequestContext) bool {
if context.IsAdmin {
return true
}
for _, membership := range context.UserMemberships {
if membership.TeamID == teamID && membership.Role == portainer.TeamLeader {
return true
}
}
return false
}
// AuthorizedUserManagement ensure that access to the management of the specified user is granted.
// It will check if the user is either administrator or the owner of the user account.
func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedRequestContext) bool {
if context.IsAdmin || context.UserID == userID {
return true
}
return false
}

View file

@ -0,0 +1,176 @@
package security
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"net/http"
"strings"
)
type (
// RequestBouncer represents an entity that manages API request accesses
RequestBouncer struct {
jwtService portainer.JWTService
teamMembershipService portainer.TeamMembershipService
authDisabled bool
}
// RestrictedRequestContext is a data structure containing information
// used in RestrictedAccess
RestrictedRequestContext struct {
IsAdmin bool
IsTeamLeader bool
UserID portainer.UserID
UserMemberships []portainer.TeamMembership
}
)
// NewRequestBouncer initializes a new RequestBouncer
func NewRequestBouncer(jwtService portainer.JWTService, teamMembershipService portainer.TeamMembershipService, authDisabled bool) *RequestBouncer {
return &RequestBouncer{
jwtService: jwtService,
teamMembershipService: teamMembershipService,
authDisabled: authDisabled,
}
}
// PublicAccess defines a security check for public endpoints.
// No authentication is required to access these endpoints.
func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
h = mwSecureHeaders(h)
return h
}
// AuthenticatedAccess defines a security check for private endpoints.
// Authentication is required to access these endpoints.
func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler {
h = bouncer.mwCheckAuthentication(h)
h = mwSecureHeaders(h)
return h
}
// RestrictedAccess defines defines a security check for restricted endpoints.
// Authentication is required to access these endpoints.
// The request context will be enhanced with a RestrictedRequestContext object
// that might be used later to authorize/filter access to resources.
func (bouncer *RequestBouncer) RestrictedAccess(h http.Handler) http.Handler {
h = bouncer.mwUpgradeToRestrictedRequest(h)
h = bouncer.AuthenticatedAccess(h)
return h
}
// AdministratorAccess defines a chain of middleware for restricted endpoints.
// Authentication as well as administrator role are required to access these endpoints.
func (bouncer *RequestBouncer) AdministratorAccess(h http.Handler) http.Handler {
h = mwCheckAdministratorRole(h)
h = bouncer.AuthenticatedAccess(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")
next.ServeHTTP(w, r)
})
}
// mwUpgradeToRestrictedRequest will enhance the current request with
// a new RestrictedRequestContext object.
func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenData, err := RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
return
}
requestContext, err := bouncer.newRestrictedContextRequest(tokenData.ID, tokenData.Role)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
return
}
ctx := storeRestrictedRequestContext(r, requestContext)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 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 := RetrieveTokenData(r)
if err != nil || tokenData.Role != portainer.AdministratorRole {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
return
}
next.ServeHTTP(w, r)
})
}
// mwCheckAuthentication provides Authentication middleware for handlers
func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var tokenData *portainer.TokenData
if !bouncer.authDisabled {
var token string
// Get token from the Authorization header
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
return
}
var err error
tokenData, err = bouncer.jwtService.ParseAndVerifyToken(token)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusUnauthorized, nil)
return
}
} else {
tokenData = &portainer.TokenData{
Role: portainer.AdministratorRole,
}
}
ctx := storeTokenData(r, tokenData)
next.ServeHTTP(w, r.WithContext(ctx))
return
})
}
func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) {
requestContext := &RestrictedRequestContext{
IsAdmin: true,
UserID: userID,
}
if userRole != portainer.AdministratorRole {
requestContext.IsAdmin = false
memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(userID)
if err != nil {
return nil, err
}
isTeamLeader := false
for _, membership := range memberships {
if membership.Role == portainer.TeamLeader {
isTeamLeader = true
}
}
requestContext.IsTeamLeader = isTeamLeader
requestContext.UserMemberships = memberships
}
return requestContext, nil
}

View file

@ -0,0 +1,50 @@
package security
import (
"context"
"net/http"
"github.com/portainer/portainer"
)
type (
contextKey int
)
const (
contextAuthenticationKey contextKey = iota
contextRestrictedRequest
)
// storeTokenData stores a TokenData object inside the request context and returns the enhanced context.
func storeTokenData(request *http.Request, tokenData *portainer.TokenData) context.Context {
return context.WithValue(request.Context(), contextAuthenticationKey, tokenData)
}
// RetrieveTokenData returns the TokenData object stored in the request context.
func RetrieveTokenData(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
}
// storeRestrictedRequestContext stores a RestrictedRequestContext object inside the request context
// and returns the enhanced context.
func storeRestrictedRequestContext(request *http.Request, requestContext *RestrictedRequestContext) context.Context {
return context.WithValue(request.Context(), contextRestrictedRequest, requestContext)
}
// RetrieveRestrictedRequestContext returns the RestrictedRequestContext object stored in the request context.
func RetrieveRestrictedRequestContext(request *http.Request) (*RestrictedRequestContext, error) {
contextData := request.Context().Value(contextRestrictedRequest)
if contextData == nil {
return nil, portainer.ErrMissingSecurityContext
}
requestContext := contextData.(*RestrictedRequestContext)
return requestContext, nil
}

View file

@ -0,0 +1,95 @@
package security
import "github.com/portainer/portainer"
// FilterUserTeams filters teams based on user role.
// non-administrator users only have access to team they are member of.
func FilterUserTeams(teams []portainer.Team, context *RestrictedRequestContext) []portainer.Team {
filteredTeams := teams
if !context.IsAdmin {
filteredTeams = make([]portainer.Team, 0)
for _, membership := range context.UserMemberships {
for _, team := range teams {
if team.ID == membership.TeamID {
filteredTeams = append(filteredTeams, team)
break
}
}
}
}
return filteredTeams
}
// FilterLeaderTeams filters teams based on user role.
// Team leaders only have access to team they lead.
func FilterLeaderTeams(teams []portainer.Team, context *RestrictedRequestContext) []portainer.Team {
filteredTeams := teams
if context.IsTeamLeader {
filteredTeams = make([]portainer.Team, 0)
for _, membership := range context.UserMemberships {
for _, team := range teams {
if team.ID == membership.TeamID && membership.Role == portainer.TeamLeader {
filteredTeams = append(filteredTeams, team)
break
}
}
}
}
return filteredTeams
}
// FilterUsers filters users based on user role.
// Non-administrator users only have access to non-administrator users.
func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []portainer.User {
filteredUsers := users
if !context.IsAdmin {
filteredUsers = make([]portainer.User, 0)
for _, user := range users {
if user.Role != portainer.AdministratorRole {
filteredUsers = append(filteredUsers, user)
}
}
}
return filteredUsers
}
// FilterEndpoints filters endpoints based on user role and team memberships.
// Non administrator users only have access to authorized endpoints.
func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestContext) ([]portainer.Endpoint, error) {
filteredEndpoints := endpoints
if !context.IsAdmin {
filteredEndpoints = make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if isEndpointAccessAuthorized(&endpoint, context.UserID, context.UserMemberships) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
}
return filteredEndpoints, nil
}
func isEndpointAccessAuthorized(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

@ -2,6 +2,9 @@ package http
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/handler"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"net/http"
)
@ -13,6 +16,8 @@ type Server struct {
AuthDisabled bool
EndpointManagement bool
UserService portainer.UserService
TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService
EndpointService portainer.EndpointService
ResourceControlService portainer.ResourceControlService
CryptoService portainer.CryptoService
@ -20,7 +25,7 @@ type Server struct {
FileService portainer.FileService
Settings *portainer.Settings
TemplatesURL string
Handler *Handler
Handler *handler.Handler
SSL bool
SSLCert string
SSLKey string
@ -28,49 +33,55 @@ type Server struct {
// Start starts the HTTP server
func (server *Server) Start() error {
middleWareService := &middleWareService{
jwtService: server.JWTService,
authDisabled: server.AuthDisabled,
}
proxyService := NewProxyService(server.ResourceControlService)
requestBouncer := security.NewRequestBouncer(server.JWTService, server.TeamMembershipService, server.AuthDisabled)
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService)
var authHandler = NewAuthHandler(middleWareService)
var authHandler = handler.NewAuthHandler(requestBouncer, server.AuthDisabled)
authHandler.UserService = server.UserService
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService
authHandler.authDisabled = server.AuthDisabled
var userHandler = NewUserHandler(middleWareService)
var userHandler = handler.NewUserHandler(requestBouncer)
userHandler.UserService = server.UserService
userHandler.TeamService = server.TeamService
userHandler.TeamMembershipService = server.TeamMembershipService
userHandler.CryptoService = server.CryptoService
userHandler.ResourceControlService = server.ResourceControlService
var settingsHandler = NewSettingsHandler(middleWareService)
settingsHandler.settings = server.Settings
var templatesHandler = NewTemplatesHandler(middleWareService)
templatesHandler.containerTemplatesURL = server.TemplatesURL
var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
var teamHandler = handler.NewTeamHandler(requestBouncer)
teamHandler.TeamService = server.TeamService
teamHandler.TeamMembershipService = server.TeamMembershipService
var teamMembershipHandler = handler.NewTeamMembershipHandler(requestBouncer)
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
var settingsHandler = handler.NewSettingsHandler(requestBouncer, server.Settings)
var templatesHandler = handler.NewTemplatesHandler(requestBouncer, server.TemplatesURL)
var dockerHandler = handler.NewDockerHandler(requestBouncer)
dockerHandler.EndpointService = server.EndpointService
dockerHandler.ProxyService = proxyService
var websocketHandler = NewWebSocketHandler()
dockerHandler.TeamMembershipService = server.TeamMembershipService
dockerHandler.ProxyManager = proxyManager
var websocketHandler = handler.NewWebSocketHandler()
websocketHandler.EndpointService = server.EndpointService
var endpointHandler = NewEndpointHandler(middleWareService)
endpointHandler.authorizeEndpointManagement = server.EndpointManagement
var endpointHandler = handler.NewEndpointHandler(requestBouncer, server.EndpointManagement)
endpointHandler.EndpointService = server.EndpointService
endpointHandler.FileService = server.FileService
endpointHandler.ProxyService = proxyService
var uploadHandler = NewUploadHandler(middleWareService)
endpointHandler.ProxyManager = proxyManager
var resourceHandler = handler.NewResourceHandler(requestBouncer)
resourceHandler.ResourceControlService = server.ResourceControlService
var uploadHandler = handler.NewUploadHandler(requestBouncer)
uploadHandler.FileService = server.FileService
var fileHandler = newFileHandler(server.AssetsPath)
var fileHandler = handler.NewFileHandler(server.AssetsPath)
server.Handler = &Handler{
AuthHandler: authHandler,
UserHandler: userHandler,
EndpointHandler: endpointHandler,
SettingsHandler: settingsHandler,
TemplatesHandler: templatesHandler,
DockerHandler: dockerHandler,
WebSocketHandler: websocketHandler,
FileHandler: fileHandler,
UploadHandler: uploadHandler,
server.Handler = &handler.Handler{
AuthHandler: authHandler,
UserHandler: userHandler,
TeamHandler: teamHandler,
TeamMembershipHandler: teamMembershipHandler,
EndpointHandler: endpointHandler,
ResourceHandler: resourceHandler,
SettingsHandler: settingsHandler,
TemplatesHandler: templatesHandler,
DockerHandler: dockerHandler,
WebSocketHandler: websocketHandler,
FileHandler: fileHandler,
UploadHandler: uploadHandler,
}
if server.SSL {

View file

@ -1,26 +0,0 @@
package http
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
)
// createTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
func createTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}
caCert, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
config := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
return config, nil
}

View file

@ -1,480 +0,0 @@
package http
import (
"strconv"
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"os"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// UserHandler represents an HTTP API handler for managing users.
type UserHandler struct {
*mux.Router
Logger *log.Logger
UserService portainer.UserService
ResourceControlService portainer.ResourceControlService
CryptoService portainer.CryptoService
}
// NewUserHandler returns a new instance of UserHandler.
func NewUserHandler(mw *middleWareService) *UserHandler {
h := &UserHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
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) {
var req postUsersRequest
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
}
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 {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.CreateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type postUsersRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
Role int `valid:"required"`
}
// 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})
return
}
vars := mux.Vars(r)
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 {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
var password = req.Password
u, 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
}
valid := true
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
valid = false
}
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
}
type postUserPasswdRequest struct {
Password string `valid:"required"`
}
type postUserPasswdResponse struct {
Valid bool `json:"valid"`
}
// handleGetUser handles GET requests on /users/:id
func (handler *UserHandler) handleGetUser(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
}
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
}
user.Password = ""
encodeJSON(w, &user, handler.Logger)
}
// 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 {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
if req.Password == "" && req.Role == 0 {
Error(w, ErrInvalidRequestFormat, 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
} 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
}
}
type putUserRequest struct {
Password string `valid:"-"`
Role int `valid:"-"`
}
// handlePostAdminInit handles GET requests on /users/admin/check
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
handleNotAllowed(w, []string{http.MethodGet})
return
}
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if len(users) == 0 {
Error(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
return
}
}
// handlePostAdminInit handles POST requests on /users/admin/init
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
return
}
var req postAdminInitRequest
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
}
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 {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.CreateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if user != nil {
Error(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger)
return
}
}
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
}
}