mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
chore: add workspace proxies to the backend (#7032)
Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
37
coderd/httpmw/actor.go
Normal file
37
coderd/httpmw/actor.go
Normal file
@ -0,0 +1,37 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
// RequireAPIKeyOrWorkspaceProxyAuth is middleware that should be inserted after
|
||||
// optional ExtractAPIKey and ExtractWorkspaceProxy middlewares to ensure one of
|
||||
// the two authentication methods is provided.
|
||||
//
|
||||
// If both are provided, an error is returned to avoid misuse.
|
||||
func RequireAPIKeyOrWorkspaceProxyAuth() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, hasAPIKey := APIKeyOptional(r)
|
||||
_, hasWorkspaceProxy := WorkspaceProxyOptional(r)
|
||||
|
||||
if hasAPIKey && hasWorkspaceProxy {
|
||||
httpapi.Write(r.Context(), w, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "API key and external proxy authentication provided, but only one is allowed",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !hasAPIKey && !hasWorkspaceProxy {
|
||||
httpapi.Write(r.Context(), w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "API key or external proxy authentication required, but none provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
143
coderd/httpmw/actor_test.go
Normal file
143
coderd/httpmw/actor_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
package httpmw_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/dbfake"
|
||||
"github.com/coder/coder/coderd/database/dbgen"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func TestRequireAPIKeyOrWorkspaceProxyAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("None", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
httpmw.RequireAPIKeyOrWorkspaceProxyAuth()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Error("should not have been called")
|
||||
})).ServeHTTP(rw, r)
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, rw.Code)
|
||||
})
|
||||
|
||||
t.Run("APIKey", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
db = dbfake.New()
|
||||
user = dbgen.User(t, db, database.User{})
|
||||
_, token = dbgen.APIKey(t, db, database.APIKey{
|
||||
UserID: user.ID,
|
||||
ExpiresAt: database.Now().AddDate(0, 0, 1),
|
||||
})
|
||||
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.Header.Set(codersdk.SessionTokenHeader, token)
|
||||
|
||||
var called int64
|
||||
httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
|
||||
DB: db,
|
||||
RedirectToLogin: false,
|
||||
})(
|
||||
httpmw.RequireAPIKeyOrWorkspaceProxyAuth()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt64(&called, 1)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}))).
|
||||
ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
dump, err := httputil.DumpResponse(res, true)
|
||||
require.NoError(t, err)
|
||||
t.Log(string(dump))
|
||||
|
||||
require.Equal(t, http.StatusOK, rw.Code)
|
||||
require.Equal(t, int64(1), atomic.LoadInt64(&called))
|
||||
})
|
||||
|
||||
t.Run("WorkspaceProxy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
db = dbfake.New()
|
||||
user = dbgen.User(t, db, database.User{})
|
||||
_, userToken = dbgen.APIKey(t, db, database.APIKey{
|
||||
UserID: user.ID,
|
||||
ExpiresAt: database.Now().AddDate(0, 0, 1),
|
||||
})
|
||||
proxy, proxyToken = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{})
|
||||
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.Header.Set(codersdk.SessionTokenHeader, userToken)
|
||||
r.Header.Set(httpmw.WorkspaceProxyAuthTokenHeader, fmt.Sprintf("%s:%s", proxy.ID, proxyToken))
|
||||
|
||||
httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
|
||||
DB: db,
|
||||
RedirectToLogin: false,
|
||||
})(
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: db,
|
||||
})(
|
||||
httpmw.RequireAPIKeyOrWorkspaceProxyAuth()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})))).
|
||||
ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
dump, err := httputil.DumpResponse(res, true)
|
||||
require.NoError(t, err)
|
||||
t.Log(string(dump))
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rw.Code)
|
||||
})
|
||||
|
||||
t.Run("Both", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
db = dbfake.New()
|
||||
proxy, token = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{})
|
||||
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.Header.Set(httpmw.WorkspaceProxyAuthTokenHeader, fmt.Sprintf("%s:%s", proxy.ID, token))
|
||||
|
||||
var called int64
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: db,
|
||||
})(
|
||||
httpmw.RequireAPIKeyOrWorkspaceProxyAuth()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt64(&called, 1)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}))).
|
||||
ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
dump, err := httputil.DumpResponse(res, true)
|
||||
require.NoError(t, err)
|
||||
t.Log(string(dump))
|
||||
|
||||
require.Equal(t, http.StatusOK, rw.Code)
|
||||
require.Equal(t, int64(1), atomic.LoadInt64(&called))
|
||||
})
|
||||
}
|
@ -47,9 +47,10 @@ type userAuthKey struct{}
|
||||
|
||||
type Authorization struct {
|
||||
Actor rbac.Subject
|
||||
// Username is required for logging and human friendly related
|
||||
// identification.
|
||||
Username string
|
||||
// ActorName is required for logging and human friendly related identification.
|
||||
// It is usually the "username" of the user, but it can be the name of the
|
||||
// external workspace proxy or other service type actor.
|
||||
ActorName string
|
||||
}
|
||||
|
||||
// UserAuthorizationOptional may return the roles and scope used for
|
||||
@ -99,6 +100,10 @@ type ExtractAPIKeyConfig struct {
|
||||
// will be deleted and the request will continue. If the request is not a
|
||||
// cookie-based request, the request will be rejected with a 401.
|
||||
Optional bool
|
||||
|
||||
// SessionTokenFunc is a custom function that can be used to extract the API
|
||||
// key. If nil, the default behavior is used.
|
||||
SessionTokenFunc func(r *http.Request) string
|
||||
}
|
||||
|
||||
// ExtractAPIKeyMW calls ExtractAPIKey with the given config on each request,
|
||||
@ -145,7 +150,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
|
||||
// like workspace applications.
|
||||
write := func(code int, response codersdk.Response) (*database.APIKey, *Authorization, bool) {
|
||||
if cfg.RedirectToLogin {
|
||||
RedirectToLogin(rw, r, response.Message)
|
||||
RedirectToLogin(rw, r, nil, response.Message)
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
@ -167,7 +172,11 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
token := apiTokenFromRequest(r)
|
||||
tokenFunc := APITokenFromRequest
|
||||
if cfg.SessionTokenFunc != nil {
|
||||
tokenFunc = cfg.SessionTokenFunc
|
||||
}
|
||||
token := tokenFunc(r)
|
||||
if token == "" {
|
||||
return optionalWrite(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: SignedOutErrorMessage,
|
||||
@ -364,7 +373,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
|
||||
|
||||
// Actor is the user's authorization context.
|
||||
authz := Authorization{
|
||||
Username: roles.Username,
|
||||
ActorName: roles.Username,
|
||||
Actor: rbac.Subject{
|
||||
ID: key.UserID.String(),
|
||||
Roles: rbac.RoleNames(roles.Roles),
|
||||
@ -376,14 +385,14 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
|
||||
return &key, &authz, true
|
||||
}
|
||||
|
||||
// apiTokenFromRequest returns the api token from the request.
|
||||
// APITokenFromRequest returns the api token from the request.
|
||||
// Find the session token from:
|
||||
// 1: The cookie
|
||||
// 1: The devurl cookie
|
||||
// 3: The old cookie
|
||||
// 4. The coder_session_token query parameter
|
||||
// 5. The custom auth header
|
||||
func apiTokenFromRequest(r *http.Request) string {
|
||||
func APITokenFromRequest(r *http.Request) string {
|
||||
cookie, err := r.Cookie(codersdk.SessionTokenCookie)
|
||||
if err == nil && cookie.Value != "" {
|
||||
return cookie.Value
|
||||
@ -432,7 +441,11 @@ func SplitAPIToken(token string) (id string, secret string, err error) {
|
||||
|
||||
// RedirectToLogin redirects the user to the login page with the `message` and
|
||||
// `redirect` query parameters set.
|
||||
func RedirectToLogin(rw http.ResponseWriter, r *http.Request, message string) {
|
||||
//
|
||||
// If dashboardURL is nil, the redirect will be relative to the current
|
||||
// request's host. If it is not nil, the redirect will be absolute with dashboard
|
||||
// url as the host.
|
||||
func RedirectToLogin(rw http.ResponseWriter, r *http.Request, dashboardURL *url.URL, message string) {
|
||||
path := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
path += "?" + r.URL.RawQuery
|
||||
@ -446,6 +459,16 @@ func RedirectToLogin(rw http.ResponseWriter, r *http.Request, message string) {
|
||||
Path: "/login",
|
||||
RawQuery: q.Encode(),
|
||||
}
|
||||
// If dashboardURL is provided, we want to redirect to the dashboard
|
||||
// login page.
|
||||
if dashboardURL != nil {
|
||||
cpy := *dashboardURL
|
||||
cpy.Path = u.Path
|
||||
cpy.RawQuery = u.RawQuery
|
||||
u = &cpy
|
||||
}
|
||||
|
||||
http.Redirect(rw, r, u.String(), http.StatusTemporaryRedirect)
|
||||
// See other forces a GET request rather than keeping the current method
|
||||
// (like temporary redirect does).
|
||||
http.Redirect(rw, r, u.String(), http.StatusSeeOther)
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ func TestAPIKey(t *testing.T) {
|
||||
location, err := res.Location()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, location.Query().Get("message"))
|
||||
require.Equal(t, http.StatusTemporaryRedirect, res.StatusCode)
|
||||
require.Equal(t, http.StatusSeeOther, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InvalidFormat", func(t *testing.T) {
|
||||
@ -526,7 +526,7 @@ func TestAPIKey(t *testing.T) {
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusTemporaryRedirect, res.StatusCode)
|
||||
require.Equal(t, http.StatusSeeOther, res.StatusCode)
|
||||
u, err := res.Location()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "/login", u.Path)
|
||||
|
@ -57,7 +57,7 @@ func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Han
|
||||
apiKey, ok := APIKeyOptional(r)
|
||||
if !ok {
|
||||
if redirectToLoginOnMe {
|
||||
RedirectToLogin(rw, r, SignedOutErrorMessage)
|
||||
RedirectToLogin(rw, r, nil, SignedOutErrorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
tokenValue := apiTokenFromRequest(r)
|
||||
tokenValue := APITokenFromRequest(r)
|
||||
if tokenValue == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.SessionTokenCookie),
|
||||
|
158
coderd/httpmw/workspaceproxy.go
Normal file
158
coderd/httpmw/workspaceproxy.go
Normal file
@ -0,0 +1,158 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
const (
|
||||
// WorkspaceProxyAuthTokenHeader is the auth header used for requests from
|
||||
// external workspace proxies.
|
||||
//
|
||||
// The format of an external proxy token is:
|
||||
// <proxy id>:<proxy secret>
|
||||
//
|
||||
//nolint:gosec
|
||||
WorkspaceProxyAuthTokenHeader = "Coder-External-Proxy-Token"
|
||||
)
|
||||
|
||||
type workspaceProxyContextKey struct{}
|
||||
|
||||
// WorkspaceProxyOptional may return the workspace proxy from the ExtractWorkspaceProxy
|
||||
// middleware.
|
||||
func WorkspaceProxyOptional(r *http.Request) (database.WorkspaceProxy, bool) {
|
||||
proxy, ok := r.Context().Value(workspaceProxyContextKey{}).(database.WorkspaceProxy)
|
||||
return proxy, ok
|
||||
}
|
||||
|
||||
// WorkspaceProxy returns the workspace proxy from the ExtractWorkspaceProxy
|
||||
// middleware.
|
||||
func WorkspaceProxy(r *http.Request) database.WorkspaceProxy {
|
||||
proxy, ok := WorkspaceProxyOptional(r)
|
||||
if !ok {
|
||||
panic("developer error: ExtractWorkspaceProxy middleware not provided")
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
|
||||
type ExtractWorkspaceProxyConfig struct {
|
||||
DB database.Store
|
||||
// Optional indicates whether the middleware should be optional. If true,
|
||||
// any requests without the external proxy auth token header will be
|
||||
// allowed to continue and no workspace proxy will be set on the request
|
||||
// context.
|
||||
Optional bool
|
||||
}
|
||||
|
||||
// ExtractWorkspaceProxy extracts the external workspace proxy from the request
|
||||
// using the external proxy auth token header.
|
||||
func ExtractWorkspaceProxy(opts ExtractWorkspaceProxyConfig) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
token := r.Header.Get(WorkspaceProxyAuthTokenHeader)
|
||||
if token == "" {
|
||||
if opts.Optional {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "Missing required external proxy token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Split the token and lookup the corresponding workspace proxy.
|
||||
parts := strings.Split(token, ":")
|
||||
if len(parts) != 2 {
|
||||
httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "Invalid external proxy token",
|
||||
})
|
||||
return
|
||||
}
|
||||
proxyID, err := uuid.Parse(parts[0])
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "Invalid external proxy token",
|
||||
})
|
||||
return
|
||||
}
|
||||
secret := parts[1]
|
||||
if len(secret) != 64 {
|
||||
httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "Invalid external proxy token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the proxy.
|
||||
// nolint:gocritic // Get proxy by ID to check auth token
|
||||
proxy, err := opts.DB.GetWorkspaceProxyByID(dbauthz.AsSystemRestricted(ctx), proxyID)
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
// Proxy IDs are public so we don't care about leaking them via
|
||||
// timing attacks.
|
||||
httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "Invalid external proxy token",
|
||||
Detail: "Proxy not found.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
if proxy.Deleted {
|
||||
httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "Invalid external proxy token",
|
||||
Detail: "Proxy has been deleted.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Do a subtle constant time comparison of the hash of the secret.
|
||||
hashedSecret := sha256.Sum256([]byte(secret))
|
||||
if subtle.ConstantTimeCompare(proxy.TokenHashedSecret, hashedSecret[:]) != 1 {
|
||||
httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "Invalid external proxy token",
|
||||
Detail: "Invalid proxy token secret.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx = r.Context()
|
||||
ctx = context.WithValue(ctx, workspaceProxyContextKey{}, proxy)
|
||||
//nolint:gocritic // Workspace proxies have full permissions. The
|
||||
// workspace proxy auth middleware is not mounted to every route, so
|
||||
// they can still only access the routes that the middleware is
|
||||
// mounted to.
|
||||
ctx = dbauthz.AsSystemRestricted(ctx)
|
||||
subj, ok := dbauthz.ActorFromContext(ctx)
|
||||
if !ok {
|
||||
// This should never happen
|
||||
httpapi.InternalServerError(w, xerrors.New("developer error: ExtractWorkspaceProxy missing rbac actor"))
|
||||
return
|
||||
}
|
||||
// Use the same subject for the userAuthKey
|
||||
ctx = context.WithValue(ctx, userAuthKey{}, Authorization{
|
||||
Actor: subj,
|
||||
ActorName: "proxy_" + proxy.Name,
|
||||
})
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
163
coderd/httpmw/workspaceproxy_test.go
Normal file
163
coderd/httpmw/workspaceproxy_test.go
Normal file
@ -0,0 +1,163 @@
|
||||
package httpmw_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/dbfake"
|
||||
"github.com/coder/coder/coderd/database/dbgen"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
)
|
||||
|
||||
func TestExtractWorkspaceProxy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
successHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Only called if the API key passes through the handler.
|
||||
httpapi.Write(context.Background(), rw, http.StatusOK, codersdk.Response{
|
||||
Message: "It worked!",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("NoHeader", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
db = dbfake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: db,
|
||||
})(successHandler).ServeHTTP(rw, r)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InvalidFormat", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
db = dbfake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.Header.Set(httpmw.WorkspaceProxyAuthTokenHeader, "test:wow-hello")
|
||||
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: db,
|
||||
})(successHandler).ServeHTTP(rw, r)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InvalidID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
db = dbfake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.Header.Set(httpmw.WorkspaceProxyAuthTokenHeader, "test:wow")
|
||||
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: db,
|
||||
})(successHandler).ServeHTTP(rw, r)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InvalidSecretLength", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
db = dbfake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.Header.Set(httpmw.WorkspaceProxyAuthTokenHeader, fmt.Sprintf("%s:%s", uuid.NewString(), "wow"))
|
||||
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: db,
|
||||
})(successHandler).ServeHTTP(rw, r)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
db = dbfake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
|
||||
secret, err := cryptorand.HexString(64)
|
||||
require.NoError(t, err)
|
||||
r.Header.Set(httpmw.WorkspaceProxyAuthTokenHeader, fmt.Sprintf("%s:%s", uuid.NewString(), secret))
|
||||
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: db,
|
||||
})(successHandler).ServeHTTP(rw, r)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InvalidSecret", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
db = dbfake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
|
||||
proxy, _ = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{})
|
||||
)
|
||||
|
||||
// Use a different secret so they don't match!
|
||||
secret, err := cryptorand.HexString(64)
|
||||
require.NoError(t, err)
|
||||
r.Header.Set(httpmw.WorkspaceProxyAuthTokenHeader, fmt.Sprintf("%s:%s", proxy.ID.String(), secret))
|
||||
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: db,
|
||||
})(successHandler).ServeHTTP(rw, r)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
db = dbfake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
|
||||
proxy, secret = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{})
|
||||
)
|
||||
r.Header.Set(httpmw.WorkspaceProxyAuthTokenHeader, fmt.Sprintf("%s:%s", proxy.ID.String(), secret))
|
||||
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: db,
|
||||
})(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Checks that it exists on the context!
|
||||
_ = httpmw.WorkspaceProxy(r)
|
||||
successHandler.ServeHTTP(rw, r)
|
||||
})).ServeHTTP(rw, r)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user