diff --git a/api/http/csrf/csrf.go b/api/http/csrf/csrf.go index d7b0516b1..487a4abfb 100644 --- a/api/http/csrf/csrf.go +++ b/api/http/csrf/csrf.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "fmt" "net/http" + "os" httperror "github.com/portainer/portainer/pkg/libhttp/error" @@ -13,6 +14,13 @@ import ( ) 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) token := make([]byte, 32) @@ -27,7 +35,7 @@ func WithProtect(handler http.Handler) (http.Handler, error) { gorillacsrf.Secure(false), )(handler) - return withSkipCSRF(handler), nil + return withSkipCSRF(handler, isDockerDesktopExtension), nil } func withSendCSRFToken(handler http.Handler) http.Handler { @@ -48,10 +56,10 @@ 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) { - skip, err := security.ShouldSkipCSRFCheck(r) + skip, err := security.ShouldSkipCSRFCheck(r, isDockerDesktopExtension) if err != nil { httperror.WriteError(w, http.StatusForbidden, err.Error(), err) return diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 4b7095373..56edd7f20 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -528,7 +528,12 @@ func (bouncer *RequestBouncer) EdgeComputeOperation(next http.Handler) http.Hand // - public routes // - kubectl - a bearer token is needed, and no csrf token can be sent // - 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) hasCookie := cookie != nil && cookie.Value != "" diff --git a/api/http/security/bouncer_test.go b/api/http/security/bouncer_test.go index 20220e588..b202d63c9 100644 --- a/api/http/security/bouncer_test.go +++ b/api/http/security/bouncer_test.go @@ -386,40 +386,52 @@ func Test_apiKeyLookup(t *testing.T) { func Test_ShouldSkipCSRFCheck(t *testing.T) { tt := []struct { - name string - cookieValue string - apiKey string - authHeader string - expectedResult bool - expectedError bool + name string + cookieValue string + apiKey string + authHeader string + isDockerDesktopExtension bool + expectedResult bool + expectedError bool }{ { - name: "Should return false when cookie is present", - cookieValue: "test-cookie", + name: "Should return false (not skip) when cookie is present", + cookieValue: "test-cookie", + isDockerDesktopExtension: false, }, { - name: "Should return true when cookie is not present", - cookieValue: "", - expectedResult: true, + name: "Should return true (skip) when cookie is present and docker desktop extension is true", + cookieValue: "test-cookie", + isDockerDesktopExtension: true, + expectedResult: true, }, { - name: "Should return true when api key is present", - cookieValue: "", - apiKey: "test-api-key", - expectedResult: true, + name: "Should return true (skip) when cookie is not present", + cookieValue: "", + isDockerDesktopExtension: false, + expectedResult: true, }, { - name: "Should return true when auth header is present", - cookieValue: "", - authHeader: "test-auth-header", - expectedResult: true, + name: "Should return true (skip) when api key is present", + cookieValue: "", + apiKey: "test-api-key", + isDockerDesktopExtension: false, + expectedResult: true, }, { - name: "Should return false and error when both api key and auth header are present", - cookieValue: "", - apiKey: "test-api-key", - authHeader: "test-auth-header", - expectedError: true, + name: "Should return true (skip) when auth header is present", + cookieValue: "", + authHeader: "test-auth-header", + isDockerDesktopExtension: false, + expectedResult: true, + }, + { + name: "Should return false (not skip) and error when both api key and auth header are present", + cookieValue: "", + apiKey: "test-api-key", + authHeader: "test-auth-header", + isDockerDesktopExtension: false, + expectedError: true, }, } @@ -437,7 +449,7 @@ func Test_ShouldSkipCSRFCheck(t *testing.T) { req.Header.Set(jwtTokenHeader, test.authHeader) } - result, err := ShouldSkipCSRFCheck(req) + result, err := ShouldSkipCSRFCheck(req, test.isDockerDesktopExtension) is.Equal(test.expectedResult, result) if test.expectedError { is.Error(err) diff --git a/app/index.html b/app/index.html index c27b020dd..5a21bd71b 100644 --- a/app/index.html +++ b/app/index.html @@ -11,7 +11,8 @@