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:
parent
a380fd9adc
commit
5523fc9023
160 changed files with 7112 additions and 3166 deletions
|
@ -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)
|
||||
}
|
|
@ -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
30
api/http/error/error.go
Normal 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)})
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
|
94
api/http/handler/docker.go
Normal file
94
api/http/handler/docker.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
256
api/http/handler/resource_control.go
Normal file
256
api/http/handler/resource_control.go
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
252
api/http/handler/team.go
Normal 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)
|
||||
}
|
240
api/http/handler/team_membership.go
Normal file
240
api/http/handler/team_membership.go
Normal 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
|
||||
}
|
||||
}
|
|
@ -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")
|
|
@ -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
490
api/http/handler/user.go
Normal 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)
|
||||
}
|
|
@ -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 {
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
21
api/http/proxy/access_control.go
Normal file
21
api/http/proxy/access_control.go
Normal 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
|
||||
}
|
98
api/http/proxy/containers.go
Normal file
98
api/http/proxy/containers.go
Normal 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
|
||||
}
|
90
api/http/proxy/decorator.go
Normal file
90
api/http/proxy/decorator.go
Normal 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
55
api/http/proxy/factory.go
Normal 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
91
api/http/proxy/filter.go
Normal 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
68
api/http/proxy/manager.go
Normal 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)
|
||||
}
|
90
api/http/proxy/response.go
Normal file
90
api/http/proxy/response.go
Normal 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
|
||||
}
|
46
api/http/proxy/reverse_proxy.go
Normal file
46
api/http/proxy/reverse_proxy.go
Normal 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
64
api/http/proxy/service.go
Normal 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
40
api/http/proxy/socket.go
Normal 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
237
api/http/proxy/transport.go
Normal 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
17
api/http/proxy/utils.go
Normal 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
73
api/http/proxy/volumes.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
123
api/http/security/authorization.go
Normal file
123
api/http/security/authorization.go
Normal 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
|
||||
}
|
176
api/http/security/bouncer.go
Normal file
176
api/http/security/bouncer.go
Normal 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
|
||||
}
|
50
api/http/security/context.go
Normal file
50
api/http/security/context.go
Normal 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
|
||||
}
|
95
api/http/security/filter.go
Normal file
95
api/http/security/filter.go
Normal 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue