mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
363 lines
8.9 KiB
Go
363 lines
8.9 KiB
Go
package workspaceapps_test
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-jose/go-jose/v4/jwt"
|
|
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/jwtutils"
|
|
"github.com/coder/coder/v2/coderd/workspaceapps"
|
|
)
|
|
|
|
func Test_TokenMatchesRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
req workspaceapps.Request
|
|
token workspaceapps.SignedToken
|
|
want bool
|
|
}{
|
|
{
|
|
name: "OK",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "qux",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "qux",
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "NormalizePath",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "qux",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
// With trailing slash
|
|
BasePath: "/app/",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "qux",
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "DifferentAccessMethod",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "DifferentBasePath",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "DifferentUsernameOrID",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "bar",
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "DifferentWorkspaceNameOrID",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "baz",
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "DifferentAgentNameOrID",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "qux",
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "DifferentAppSlugOrPort",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "qux",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodPath,
|
|
BasePath: "/app",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "quux",
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "SamePrefix",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
Prefix: "dean-was--here---",
|
|
BasePath: "/",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "qux",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
Prefix: "dean--was--here---",
|
|
BasePath: "/",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "quux",
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "DifferentPrefix",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
Prefix: "yolo--",
|
|
BasePath: "/",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "qux",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
Prefix: "swag--",
|
|
BasePath: "/",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "quux",
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "PortPortocolHTTP",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
Prefix: "yolo--",
|
|
BasePath: "/",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "8080",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
Prefix: "yolo--",
|
|
BasePath: "/",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "8080",
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "PortPortocolHTTPS",
|
|
req: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
Prefix: "yolo--",
|
|
BasePath: "/",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "8080s",
|
|
},
|
|
token: workspaceapps.SignedToken{
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
Prefix: "yolo--",
|
|
BasePath: "/",
|
|
UsernameOrID: "foo",
|
|
WorkspaceNameOrID: "bar",
|
|
AgentNameOrID: "baz",
|
|
AppSlugOrPort: "8080s",
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
c := c
|
|
|
|
t.Run(c.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.Equal(t, c.want, c.token.MatchesRequest(c.req))
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_FromRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("MultipleTokens", func(t *testing.T) {
|
|
t.Parallel()
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
|
|
// Add an invalid token
|
|
r.AddCookie(&http.Cookie{
|
|
Name: codersdk.SignedAppTokenCookie,
|
|
Value: "invalid",
|
|
})
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
signer := newSigner(t)
|
|
|
|
token := workspaceapps.SignedToken{
|
|
RegisteredClaims: jwtutils.RegisteredClaims{
|
|
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
},
|
|
Request: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodSubdomain,
|
|
BasePath: "/",
|
|
UsernameOrID: "user",
|
|
WorkspaceAndAgent: "workspace/agent",
|
|
WorkspaceNameOrID: "workspace",
|
|
AgentNameOrID: "agent",
|
|
AppSlugOrPort: "app",
|
|
},
|
|
UserID: uuid.New(),
|
|
WorkspaceID: uuid.New(),
|
|
AgentID: uuid.New(),
|
|
AppURL: "/",
|
|
}
|
|
|
|
// Add an expired cookie
|
|
expired := token
|
|
expired.RegisteredClaims.Expiry = jwt.NewNumericDate(time.Now().Add(time.Hour * -1))
|
|
expiredStr, err := jwtutils.Sign(ctx, signer, expired)
|
|
require.NoError(t, err)
|
|
r.AddCookie(&http.Cookie{
|
|
Name: codersdk.SignedAppTokenCookie,
|
|
Value: expiredStr,
|
|
})
|
|
|
|
validStr, err := jwtutils.Sign(ctx, signer, token)
|
|
require.NoError(t, err)
|
|
|
|
r.AddCookie(&http.Cookie{
|
|
Name: codersdk.SignedAppTokenCookie,
|
|
Value: validStr,
|
|
})
|
|
|
|
signed, ok := workspaceapps.FromRequest(r, signer)
|
|
require.True(t, ok, "expected a token to be found")
|
|
// Confirm it is the correct token.
|
|
require.Equal(t, signed.UserID, token.UserID)
|
|
})
|
|
}
|
|
|
|
func newSigner(t *testing.T) jwtutils.StaticKey {
|
|
t.Helper()
|
|
|
|
return jwtutils.StaticKey{
|
|
ID: "test",
|
|
Key: generateSecret(t, 64),
|
|
}
|
|
}
|
|
|
|
func generateSecret(t *testing.T, size int) []byte {
|
|
t.Helper()
|
|
|
|
secret := make([]byte, size)
|
|
_, err := rand.Read(secret)
|
|
require.NoError(t, err)
|
|
return secret
|
|
}
|