mirror of https://github.com/portainer/portainer
fix(docker-desktop): support auth cookies [BE-11134] (#12108)
parent
8cd53a4b7a
commit
f016b31388
|
@ -4,6 +4,7 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
@ -13,6 +14,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithProtect(handler http.Handler) (http.Handler, error) {
|
func WithProtect(handler http.Handler) (http.Handler, error) {
|
||||||
|
// IsDockerDesktopExtension is used to check if we should skip csrf checks in the request bouncer (ShouldSkipCSRFCheck)
|
||||||
|
// DOCKER_EXTENSION is set to '1' in build/docker-extension/docker-compose.yml
|
||||||
|
isDockerDesktopExtension := false
|
||||||
|
if val, ok := os.LookupEnv("DOCKER_EXTENSION"); ok && val == "1" {
|
||||||
|
isDockerDesktopExtension = true
|
||||||
|
}
|
||||||
|
|
||||||
handler = withSendCSRFToken(handler)
|
handler = withSendCSRFToken(handler)
|
||||||
|
|
||||||
token := make([]byte, 32)
|
token := make([]byte, 32)
|
||||||
|
@ -26,7 +34,7 @@ func WithProtect(handler http.Handler) (http.Handler, error) {
|
||||||
gorillacsrf.Secure(false),
|
gorillacsrf.Secure(false),
|
||||||
)(handler)
|
)(handler)
|
||||||
|
|
||||||
return withSkipCSRF(handler), nil
|
return withSkipCSRF(handler, isDockerDesktopExtension), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func withSendCSRFToken(handler http.Handler) http.Handler {
|
func withSendCSRFToken(handler http.Handler) http.Handler {
|
||||||
|
@ -45,9 +53,9 @@ func withSendCSRFToken(handler http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func withSkipCSRF(handler http.Handler) http.Handler {
|
func withSkipCSRF(handler http.Handler, isDockerDesktopExtension bool) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
skip, err := security.ShouldSkipCSRFCheck(r)
|
skip, err := security.ShouldSkipCSRFCheck(r, isDockerDesktopExtension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteError(w, http.StatusForbidden, err.Error(), err)
|
httperror.WriteError(w, http.StatusForbidden, err.Error(), err)
|
||||||
|
|
||||||
|
|
|
@ -528,7 +528,12 @@ func (bouncer *RequestBouncer) EdgeComputeOperation(next http.Handler) http.Hand
|
||||||
// - public routes
|
// - public routes
|
||||||
// - kubectl - a bearer token is needed, and no csrf token can be sent
|
// - kubectl - a bearer token is needed, and no csrf token can be sent
|
||||||
// - api token
|
// - api token
|
||||||
func ShouldSkipCSRFCheck(r *http.Request) (bool, error) {
|
// - docker desktop extension
|
||||||
|
func ShouldSkipCSRFCheck(r *http.Request, isDockerDesktopExtension bool) (bool, error) {
|
||||||
|
if isDockerDesktopExtension {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
cookie, _ := r.Cookie(portainer.AuthCookieKey)
|
cookie, _ := r.Cookie(portainer.AuthCookieKey)
|
||||||
hasCookie := cookie != nil && cookie.Value != ""
|
hasCookie := cookie != nil && cookie.Value != ""
|
||||||
|
|
||||||
|
|
|
@ -390,35 +390,47 @@ func Test_ShouldSkipCSRFCheck(t *testing.T) {
|
||||||
cookieValue string
|
cookieValue string
|
||||||
apiKey string
|
apiKey string
|
||||||
authHeader string
|
authHeader string
|
||||||
|
isDockerDesktopExtension bool
|
||||||
expectedResult bool
|
expectedResult bool
|
||||||
expectedError bool
|
expectedError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Should return false when cookie is present",
|
name: "Should return false (not skip) when cookie is present",
|
||||||
cookieValue: "test-cookie",
|
cookieValue: "test-cookie",
|
||||||
|
isDockerDesktopExtension: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Should return true when cookie is not present",
|
name: "Should return true (skip) when cookie is present and docker desktop extension is true",
|
||||||
cookieValue: "",
|
cookieValue: "test-cookie",
|
||||||
|
isDockerDesktopExtension: true,
|
||||||
expectedResult: true,
|
expectedResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Should return true when api key is present",
|
name: "Should return true (skip) when cookie is not present",
|
||||||
|
cookieValue: "",
|
||||||
|
isDockerDesktopExtension: false,
|
||||||
|
expectedResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should return true (skip) when api key is present",
|
||||||
cookieValue: "",
|
cookieValue: "",
|
||||||
apiKey: "test-api-key",
|
apiKey: "test-api-key",
|
||||||
|
isDockerDesktopExtension: false,
|
||||||
expectedResult: true,
|
expectedResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Should return true when auth header is present",
|
name: "Should return true (skip) when auth header is present",
|
||||||
cookieValue: "",
|
cookieValue: "",
|
||||||
authHeader: "test-auth-header",
|
authHeader: "test-auth-header",
|
||||||
|
isDockerDesktopExtension: false,
|
||||||
expectedResult: true,
|
expectedResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Should return false and error when both api key and auth header are present",
|
name: "Should return false (not skip) and error when both api key and auth header are present",
|
||||||
cookieValue: "",
|
cookieValue: "",
|
||||||
apiKey: "test-api-key",
|
apiKey: "test-api-key",
|
||||||
authHeader: "test-auth-header",
|
authHeader: "test-auth-header",
|
||||||
|
isDockerDesktopExtension: false,
|
||||||
expectedError: true,
|
expectedError: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -437,7 +449,7 @@ func Test_ShouldSkipCSRFCheck(t *testing.T) {
|
||||||
req.Header.Set(jwtTokenHeader, test.authHeader)
|
req.Header.Set(jwtTokenHeader, test.authHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := ShouldSkipCSRFCheck(req)
|
result, err := ShouldSkipCSRFCheck(req, test.isDockerDesktopExtension)
|
||||||
is.Equal(test.expectedResult, result)
|
is.Equal(test.expectedResult, result)
|
||||||
if test.expectedError {
|
if test.expectedError {
|
||||||
is.Error(err)
|
is.Error(err)
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<base id="base" />
|
<base id="base" />
|
||||||
<script>
|
<script>
|
||||||
if (window.origin == 'file://') {
|
// http://localhost:49000 is a docker extension specific url (see /build/docker-extension/docker-compose.yml)
|
||||||
|
if (window.origin == 'http://localhost:49000') {
|
||||||
// we are loading the app from a local file as in docker extension
|
// we are loading the app from a local file as in docker extension
|
||||||
document.getElementById('base').href = 'http://localhost:49000/';
|
document.getElementById('base').href = 'http://localhost:49000/';
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,12 @@ export function HomeView() {
|
||||||
We could not connect your local environment to Portainer.
|
We could not connect your local environment to Portainer.
|
||||||
<br />
|
<br />
|
||||||
Please ensure your environment is correctly exposed. For
|
Please ensure your environment is correctly exposed. For
|
||||||
help with installation visit
|
help with installation visit{' '}
|
||||||
<a href="https://documentation.portainer.io/quickstart/">
|
<a
|
||||||
|
href="https://documentation.portainer.io/quickstart/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
https://documentation.portainer.io/quickstart
|
https://documentation.portainer.io/quickstart
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -20,7 +20,10 @@ build-remote:
|
||||||
docker buildx build -f build/linux/Dockerfile --push --builder=buildx-multi-arch --platform=windows/amd64,linux/amd64,linux/arm64 --build-arg TAG=$(VERSION) --build-arg PORTAINER_IMAGE_NAME=$(IMAGE_NAME) --tag=$(TAGGED_IMAGE_NAME) .
|
docker buildx build -f build/linux/Dockerfile --push --builder=buildx-multi-arch --platform=windows/amd64,linux/amd64,linux/arm64 --build-arg TAG=$(VERSION) --build-arg PORTAINER_IMAGE_NAME=$(IMAGE_NAME) --tag=$(TAGGED_IMAGE_NAME) .
|
||||||
|
|
||||||
install:
|
install:
|
||||||
docker extension install $(TAGGED_IMAGE_NAME)
|
docker extension install $(TAGGED_IMAGE_NAME) --force
|
||||||
|
|
||||||
|
dev:
|
||||||
|
docker extension dev debug $(IMAGE_NAME)
|
||||||
|
|
||||||
multiarch:
|
multiarch:
|
||||||
docker buildx create --name=buildx-multi-arch --driver=docker-container --driver-opt=network=host
|
docker buildx create --name=buildx-multi-arch --driver=docker-container --driver-opt=network=host
|
||||||
|
|
|
@ -20,10 +20,9 @@ Next you must install the CLI plugin to enable extension development. Please fol
|
||||||
|
|
||||||
### Build from local changes
|
### Build from local changes
|
||||||
|
|
||||||
1. Run `yarn` to install the project dependencies
|
1. Run `make dev-extension` to install the project dependencies and start in development mode (note that this doesn't do live updates for frontend changes).
|
||||||
2. Run `yarn dev:extension` to install the extension
|
2. Make your code changes
|
||||||
3. Make your code changes
|
3. Re-run `make dev-extension` to rebuild and re-install with your latest changes
|
||||||
4. Re-run `yarn dev:extension` to rebuild and re-install with your latest changes
|
|
||||||
|
|
||||||
## Accessing the Portainer extension
|
## Accessing the Portainer extension
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"dashboard-tab": {
|
"dashboard-tab": {
|
||||||
"title": "Portainer",
|
"title": "Portainer",
|
||||||
"root": "/public",
|
"root": "/public",
|
||||||
"src": "index.html"
|
"src": "http://localhost:49000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue