mirror of https://github.com/portainer/portainer
fix(api): add an authenticated access policy to the websocket endpoint (#1979)
* fix(api): add an authenticated access policy to the websocket endpoint * refactor(api): centralize EndpointAccess validation * feat(api): validate id query parameter for the /websocket/exec endpointpull/1980/head^2
parent
f3ce5c25de
commit
da5a430b8c
|
@ -11,16 +11,16 @@ import (
|
||||||
// Handler is the HTTP handler used to proxy requests to external APIs.
|
// Handler is the HTTP handler used to proxy requests to external APIs.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
EndpointService portainer.EndpointService
|
requestBouncer *security.RequestBouncer
|
||||||
EndpointGroupService portainer.EndpointGroupService
|
EndpointService portainer.EndpointService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
ProxyManager *proxy.Manager
|
||||||
ProxyManager *proxy.Manager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to proxy requests to external APIs.
|
// NewHandler creates a handler to proxy requests to external APIs.
|
||||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
|
requestBouncer: bouncer,
|
||||||
}
|
}
|
||||||
h.PathPrefix("/{id}/azure").Handler(
|
h.PathPrefix("/{id}/azure").Handler(
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
|
||||||
|
@ -30,21 +30,3 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) checkEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID) error {
|
|
||||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !security.AuthorizedEndpointAccess(endpoint, group, userID, memberships) {
|
|
||||||
return portainer.ErrEndpointAccessDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
httperror "github.com/portainer/portainer/http/error"
|
httperror "github.com/portainer/portainer/http/error"
|
||||||
"github.com/portainer/portainer/http/request"
|
"github.com/portainer/portainer/http/request"
|
||||||
"github.com/portainer/portainer/http/security"
|
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -24,18 +23,9 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
|
||||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
httperror "github.com/portainer/portainer/http/error"
|
httperror "github.com/portainer/portainer/http/error"
|
||||||
"github.com/portainer/portainer/http/request"
|
"github.com/portainer/portainer/http/request"
|
||||||
"github.com/portainer/portainer/http/security"
|
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -24,18 +23,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
|
||||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
httperror "github.com/portainer/portainer/http/error"
|
httperror "github.com/portainer/portainer/http/error"
|
||||||
"github.com/portainer/portainer/http/request"
|
"github.com/portainer/portainer/http/request"
|
||||||
"github.com/portainer/portainer/http/security"
|
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -24,18 +23,9 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
|
||||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var storidgeExtension *portainer.EndpointExtension
|
var storidgeExtension *portainer.EndpointExtension
|
||||||
|
|
|
@ -14,13 +14,12 @@ import (
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
stackCreationMutex *sync.Mutex
|
stackCreationMutex *sync.Mutex
|
||||||
stackDeletionMutex *sync.Mutex
|
stackDeletionMutex *sync.Mutex
|
||||||
|
requestBouncer *security.RequestBouncer
|
||||||
*mux.Router
|
*mux.Router
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
GitService portainer.GitService
|
GitService portainer.GitService
|
||||||
StackService portainer.StackService
|
StackService portainer.StackService
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
EndpointGroupService portainer.EndpointGroupService
|
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
|
||||||
ResourceControlService portainer.ResourceControlService
|
ResourceControlService portainer.ResourceControlService
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
|
@ -34,6 +33,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
stackCreationMutex: &sync.Mutex{},
|
stackCreationMutex: &sync.Mutex{},
|
||||||
stackDeletionMutex: &sync.Mutex{},
|
stackDeletionMutex: &sync.Mutex{},
|
||||||
|
requestBouncer: bouncer,
|
||||||
}
|
}
|
||||||
h.Handle("/stacks",
|
h.Handle("/stacks",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost)
|
||||||
|
@ -49,21 +49,3 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) checkEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID) error {
|
|
||||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !security.AuthorizedEndpointAccess(endpoint, group, userID, memberships) {
|
|
||||||
return portainer.ErrEndpointAccessDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
httperror "github.com/portainer/portainer/http/error"
|
httperror "github.com/portainer/portainer/http/error"
|
||||||
"github.com/portainer/portainer/http/request"
|
"github.com/portainer/portainer/http/request"
|
||||||
"github.com/portainer/portainer/http/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
|
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
|
||||||
|
@ -47,18 +46,9 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
|
||||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch portainer.StackType(stackType) {
|
switch portainer.StackType(stackType) {
|
||||||
|
|
|
@ -105,18 +105,9 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
|
||||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stack = &portainer.Stack{
|
stack = &portainer.Stack{
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package websocket
|
package websocket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
httperror "github.com/portainer/portainer/http/error"
|
httperror "github.com/portainer/portainer/http/error"
|
||||||
|
"github.com/portainer/portainer/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler is the HTTP handler used to handle websocket operations.
|
// Handler is the HTTP handler used to handle websocket operations.
|
||||||
|
@ -14,15 +13,18 @@ type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
requestBouncer *security.RequestBouncer
|
||||||
connectionUpgrader websocket.Upgrader
|
connectionUpgrader websocket.Upgrader
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage websocket operations.
|
// NewHandler creates a handler to manage websocket operations.
|
||||||
func NewHandler() *Handler {
|
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
connectionUpgrader: websocket.Upgrader{},
|
connectionUpgrader: websocket.Upgrader{},
|
||||||
|
requestBouncer: bouncer,
|
||||||
}
|
}
|
||||||
h.Handle("/websocket/exec", httperror.LoggerHandler(h.websocketExec)).Methods(http.MethodGet)
|
h.PathPrefix("/websocket/exec").Handler(
|
||||||
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec)))
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/koding/websocketproxy"
|
"github.com/koding/websocketproxy"
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
@ -31,15 +32,19 @@ type execStartOperationPayload struct {
|
||||||
Detach bool
|
Detach bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// websocketExec handles GET requests on /websocket/exec?id=<execID>&endpointId=<endpointID>&nodeName=<nodeName>
|
// websocketExec handles GET requests on /websocket/exec?id=<execID>&endpointId=<endpointID>&nodeName=<nodeName>&token=<token>
|
||||||
// If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint.
|
// If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint.
|
||||||
// If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and
|
// If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and
|
||||||
// an ExecStart operation HTTP request will be created and hijacked.
|
// an ExecStart operation HTTP request will be created and hijacked.
|
||||||
|
// Authentication and access is controled via the mandatory token query parameter.
|
||||||
func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
execID, err := request.RetrieveQueryParameter(r, "id", false)
|
execID, err := request.RetrieveQueryParameter(r, "id", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: id", err}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: id", err}
|
||||||
}
|
}
|
||||||
|
if !govalidator.IsHexadecimal(execID) {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: id (must be hexadecimal identifier)", err}
|
||||||
|
}
|
||||||
|
|
||||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
|
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -53,6 +58,11 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||||
|
}
|
||||||
|
|
||||||
params := &webSocketExecRequestParams{
|
params := &webSocketExecRequestParams{
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
execID: execID,
|
execID: execID,
|
||||||
|
|
|
@ -166,10 +166,10 @@ func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedReques
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedEndpointAccess ensure that the user can access the specified endpoint.
|
// authorizedEndpointAccess ensure that the user can access the specified endpoint.
|
||||||
// It will check if the user is part of the authorized users or part of a team that is
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams of the endpoint and the associated group.
|
// listed in the authorized teams of the endpoint and the associated group.
|
||||||
func AuthorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||||
groupAccess := authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
groupAccess := authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
||||||
if !groupAccess {
|
if !groupAccess {
|
||||||
return authorizedAccess(userID, memberships, endpoint.AuthorizedUsers, endpoint.AuthorizedTeams)
|
return authorizedAccess(userID, memberships, endpoint.AuthorizedUsers, endpoint.AuthorizedTeams)
|
||||||
|
|
|
@ -14,9 +14,19 @@ type (
|
||||||
jwtService portainer.JWTService
|
jwtService portainer.JWTService
|
||||||
userService portainer.UserService
|
userService portainer.UserService
|
||||||
teamMembershipService portainer.TeamMembershipService
|
teamMembershipService portainer.TeamMembershipService
|
||||||
|
endpointGroupService portainer.EndpointGroupService
|
||||||
authDisabled bool
|
authDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestBouncerParams represents the required parameters to create a new RequestBouncer instance.
|
||||||
|
RequestBouncerParams struct {
|
||||||
|
JWTService portainer.JWTService
|
||||||
|
UserService portainer.UserService
|
||||||
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
|
AuthDisabled bool
|
||||||
|
}
|
||||||
|
|
||||||
// RestrictedRequestContext is a data structure containing information
|
// RestrictedRequestContext is a data structure containing information
|
||||||
// used in RestrictedAccess
|
// used in RestrictedAccess
|
||||||
RestrictedRequestContext struct {
|
RestrictedRequestContext struct {
|
||||||
|
@ -28,12 +38,13 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRequestBouncer initializes a new RequestBouncer
|
// NewRequestBouncer initializes a new RequestBouncer
|
||||||
func NewRequestBouncer(jwtService portainer.JWTService, userService portainer.UserService, teamMembershipService portainer.TeamMembershipService, authDisabled bool) *RequestBouncer {
|
func NewRequestBouncer(parameters *RequestBouncerParams) *RequestBouncer {
|
||||||
return &RequestBouncer{
|
return &RequestBouncer{
|
||||||
jwtService: jwtService,
|
jwtService: parameters.JWTService,
|
||||||
userService: userService,
|
userService: parameters.UserService,
|
||||||
teamMembershipService: teamMembershipService,
|
teamMembershipService: parameters.TeamMembershipService,
|
||||||
authDisabled: authDisabled,
|
endpointGroupService: parameters.EndpointGroupService,
|
||||||
|
authDisabled: parameters.AuthDisabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +81,36 @@ func (bouncer *RequestBouncer) AdministratorAccess(h http.Handler) http.Handler
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EndpointAccess retrieves the JWT token from the request context and verifies
|
||||||
|
// that the user can access the specified endpoint.
|
||||||
|
// An error is returned when access is denied.
|
||||||
|
func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portainer.Endpoint) error {
|
||||||
|
tokenData, err := RetrieveTokenData(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
group, err := bouncer.endpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) {
|
||||||
|
return portainer.ErrEndpointAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -120,6 +161,10 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
|
||||||
if !bouncer.authDisabled {
|
if !bouncer.authDisabled {
|
||||||
var token string
|
var token string
|
||||||
|
|
||||||
|
// Optionally, token might be set via the "token" query parameter.
|
||||||
|
// For example, in websocket requests
|
||||||
|
token = r.URL.Query().Get("token")
|
||||||
|
|
||||||
// Get token from the Authorization header
|
// Get token from the Authorization header
|
||||||
tokens, ok := r.Header["Authorization"]
|
tokens, ok := r.Header["Authorization"]
|
||||||
if ok && len(tokens) >= 1 {
|
if ok && len(tokens) >= 1 {
|
||||||
|
|
|
@ -88,7 +88,7 @@ func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.Endpoint
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
endpointGroup := getAssociatedGroup(&endpoint, groups)
|
endpointGroup := getAssociatedGroup(&endpoint, groups)
|
||||||
|
|
||||||
if AuthorizedEndpointAccess(&endpoint, endpointGroup, context.UserID, context.UserMemberships) {
|
if authorizedEndpointAccess(&endpoint, endpointGroup, context.UserID, context.UserMemberships) {
|
||||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,14 @@ type Server struct {
|
||||||
|
|
||||||
// Start starts the HTTP server
|
// Start starts the HTTP server
|
||||||
func (server *Server) Start() error {
|
func (server *Server) Start() error {
|
||||||
requestBouncer := security.NewRequestBouncer(server.JWTService, server.UserService, server.TeamMembershipService, server.AuthDisabled)
|
requestBouncerParameters := &security.RequestBouncerParams{
|
||||||
|
JWTService: server.JWTService,
|
||||||
|
UserService: server.UserService,
|
||||||
|
TeamMembershipService: server.TeamMembershipService,
|
||||||
|
EndpointGroupService: server.EndpointGroupService,
|
||||||
|
AuthDisabled: server.AuthDisabled,
|
||||||
|
}
|
||||||
|
requestBouncer := security.NewRequestBouncer(requestBouncerParameters)
|
||||||
proxyManagerParameters := &proxy.ManagerParams{
|
proxyManagerParameters := &proxy.ManagerParams{
|
||||||
ResourceControlService: server.ResourceControlService,
|
ResourceControlService: server.ResourceControlService,
|
||||||
TeamMembershipService: server.TeamMembershipService,
|
TeamMembershipService: server.TeamMembershipService,
|
||||||
|
@ -98,8 +105,6 @@ func (server *Server) Start() error {
|
||||||
|
|
||||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||||
endpointProxyHandler.EndpointService = server.EndpointService
|
endpointProxyHandler.EndpointService = server.EndpointService
|
||||||
endpointProxyHandler.EndpointGroupService = server.EndpointGroupService
|
|
||||||
endpointProxyHandler.TeamMembershipService = server.TeamMembershipService
|
|
||||||
endpointProxyHandler.ProxyManager = proxyManager
|
endpointProxyHandler.ProxyManager = proxyManager
|
||||||
|
|
||||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||||
|
@ -119,8 +124,6 @@ func (server *Server) Start() error {
|
||||||
stackHandler.FileService = server.FileService
|
stackHandler.FileService = server.FileService
|
||||||
stackHandler.StackService = server.StackService
|
stackHandler.StackService = server.StackService
|
||||||
stackHandler.EndpointService = server.EndpointService
|
stackHandler.EndpointService = server.EndpointService
|
||||||
stackHandler.EndpointGroupService = server.EndpointGroupService
|
|
||||||
stackHandler.TeamMembershipService = server.TeamMembershipService
|
|
||||||
stackHandler.ResourceControlService = server.ResourceControlService
|
stackHandler.ResourceControlService = server.ResourceControlService
|
||||||
stackHandler.SwarmStackManager = server.SwarmStackManager
|
stackHandler.SwarmStackManager = server.SwarmStackManager
|
||||||
stackHandler.ComposeStackManager = server.ComposeStackManager
|
stackHandler.ComposeStackManager = server.ComposeStackManager
|
||||||
|
@ -153,7 +156,7 @@ func (server *Server) Start() error {
|
||||||
userHandler.ResourceControlService = server.ResourceControlService
|
userHandler.ResourceControlService = server.ResourceControlService
|
||||||
userHandler.SettingsService = server.SettingsService
|
userHandler.SettingsService = server.SettingsService
|
||||||
|
|
||||||
var websocketHandler = websocket.NewHandler()
|
var websocketHandler = websocket.NewHandler(requestBouncer)
|
||||||
websocketHandler.EndpointService = server.EndpointService
|
websocketHandler.EndpointService = server.EndpointService
|
||||||
websocketHandler.SignatureService = server.SignatureService
|
websocketHandler.SignatureService = server.SignatureService
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ContainerConsoleController', ['$scope', '$transition$', 'ContainerService', 'ImageService', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ExecService', 'HttpRequestHelper',
|
.controller('ContainerConsoleController', ['$scope', '$transition$', 'ContainerService', 'ImageService', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ExecService', 'HttpRequestHelper', 'LocalStorage',
|
||||||
function ($scope, $transition$, ContainerService, ImageService, EndpointProvider, Notifications, ContainerHelper, ExecService, HttpRequestHelper) {
|
function ($scope, $transition$, ContainerService, ImageService, EndpointProvider, Notifications, ContainerHelper, ExecService, HttpRequestHelper, LocalStorage) {
|
||||||
var socket, term;
|
var socket, term;
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
|
@ -36,7 +36,8 @@ function ($scope, $transition$, ContainerService, ImageService, EndpointProvider
|
||||||
ContainerService.createExec(execConfig)
|
ContainerService.createExec(execConfig)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
execId = data.Id;
|
execId = data.Id;
|
||||||
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
|
var jwtToken = LocalStorage.getJWT();
|
||||||
|
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID() + '&token=' + jwtToken;
|
||||||
if ($transition$.params().nodeName) {
|
if ($transition$.params().nodeName) {
|
||||||
url += '&nodeName=' + $transition$.params().nodeName;
|
url += '&nodeName=' + $transition$.params().nodeName;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue