Skip to content

Commit fca202c

Browse files
committed
[public-api] Validate Workspace ID is a UUID
1 parent 50caba2 commit fca202c

File tree

4 files changed

+144
-71
lines changed

4 files changed

+144
-71
lines changed

components/common-go/namegen/workspaceid.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ package namegen
66

77
import (
88
"crypto/rand"
9+
"errors"
10+
"fmt"
911
"math/big"
1012
"regexp"
1113
"strings"
1214
)
1315

14-
// WorkspaceIDPattern generates a new workspace ID by randomly choosing
15-
var WorkspaceIDPattern = regexp.MustCompile(`^[a-z]{3,12}-[a-z]{2,16}-[a-z0-9]{8}$`)
16+
// WorkspaceIDPattern is the expected Worksapce ID pattern
17+
// gitpod-protocol/src/util/generate-workspace-id.ts is authoritative over the generation
18+
var WorkspaceIDPattern = regexp.MustCompile(`^[a-z]{3,12}-[a-z]{2,16}-[a-z0-9]{11}$`)
1619

1720
func GenerateWorkspaceID() (string, error) {
1821
s1, err := chooseRandomly(colors, 1)
@@ -23,14 +26,26 @@ func GenerateWorkspaceID() (string, error) {
2326
if err != nil {
2427
return "", err
2528
}
26-
s3, err := chooseRandomly(characters, 8)
29+
s3, err := chooseRandomly(characters, 11)
2730
if err != nil {
2831
return "", err
2932
}
3033

3134
return strings.Join([]string{s1, s2, s3}, "-"), nil
3235
}
3336

37+
var (
38+
InvalidWorkspaceID = errors.New("workspace id does not match required format")
39+
)
40+
41+
func ValidateWorkspaceID(id string) error {
42+
if !WorkspaceIDPattern.MatchString(id) {
43+
return fmt.Errorf("id '%s' does not match workspace ID regex '%s': %w", id, WorkspaceIDPattern.String(), InvalidWorkspaceID)
44+
}
45+
46+
return nil
47+
}
48+
3449
func chooseRandomly(options []string, length int) (res string, err error) {
3550
l := big.NewInt(int64(len(options)))
3651
for i := 0; i < length; i++ {

components/common-go/namegen/workspaceid_test.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,45 @@
55
package namegen_test
66

77
import (
8+
"github.com/stretchr/testify/require"
89
"testing"
910

1011
"github.com/gitpod-io/gitpod/common-go/namegen"
1112
)
1213

1314
func TestGenerateWorkspaceID(t *testing.T) {
14-
1515
for i := 0; i < 1000; i++ {
1616
name, err := namegen.GenerateWorkspaceID()
1717
if err != nil {
1818
t.Error(err)
1919
}
20-
if !namegen.WorkspaceIDPattern.MatchString(name) {
20+
21+
err = namegen.ValidateWorkspaceID(name)
22+
if err != nil {
2123
t.Errorf("The workspace id \"%s\" didn't met the expectation.", name)
2224
}
2325
}
2426
}
27+
28+
func TestValidateWorkspaceID(t *testing.T) {
29+
valid := []string{
30+
"gitpodio-gitpod-65k8jqq6up4",
31+
}
32+
for _, v := range valid {
33+
require.NoError(t, namegen.ValidateWorkspaceID(v))
34+
}
35+
36+
invalid := []string{
37+
"",
38+
"foo",
39+
"foo-bar",
40+
"fo-bo",
41+
"foo-bar-12",
42+
"foo--",
43+
"---",
44+
}
45+
for _, i := range invalid {
46+
require.Error(t, namegen.ValidateWorkspaceID(i))
47+
}
48+
49+
}

components/public-api-server/pkg/apiv1/workspace.go

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ package apiv1
77
import (
88
"context"
99
"fmt"
10-
1110
connect "github.com/bufbuild/connect-go"
11+
"github.com/gitpod-io/gitpod/common-go/log"
12+
"github.com/gitpod-io/gitpod/common-go/namegen"
1213
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
1314
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
1415
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
@@ -31,14 +32,19 @@ type WorkspaceService struct {
3132
}
3233

3334
func (s *WorkspaceService) GetWorkspace(ctx context.Context, req *connect.Request[v1.GetWorkspaceRequest]) (*connect.Response[v1.GetWorkspaceResponse], error) {
34-
logger := ctxlogrus.Extract(ctx)
35+
workspaceID, err := validateWorkspaceID(req.Msg.GetWorkspaceId())
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
logger := ctxlogrus.Extract(ctx).WithField("workspace_id", workspaceID)
3541

3642
conn, err := getConnection(ctx, s.connectionPool)
3743
if err != nil {
3844
return nil, err
3945
}
4046

41-
workspace, err := conn.GetWorkspace(ctx, req.Msg.GetWorkspaceId())
47+
workspace, err := conn.GetWorkspace(ctx, workspaceID)
4248
if err != nil {
4349
logger.WithError(err).Error("Failed to get workspace.")
4450
return nil, proxy.ConvertError(err)
@@ -71,13 +77,18 @@ func (s *WorkspaceService) GetWorkspace(ctx context.Context, req *connect.Reques
7177
}
7278

7379
func (s *WorkspaceService) GetOwnerToken(ctx context.Context, req *connect.Request[v1.GetOwnerTokenRequest]) (*connect.Response[v1.GetOwnerTokenResponse], error) {
74-
logger := ctxlogrus.Extract(ctx)
80+
workspaceID, err := validateWorkspaceID(req.Msg.GetWorkspaceId())
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
logger := ctxlogrus.Extract(ctx).WithField("workspace_id", workspaceID)
7586
conn, err := getConnection(ctx, s.connectionPool)
7687
if err != nil {
7788
return nil, err
7889
}
7990

80-
ownerToken, err := conn.GetOwnerToken(ctx, req.Msg.GetWorkspaceId())
91+
ownerToken, err := conn.GetOwnerToken(ctx, workspaceID)
8192

8293
if err != nil {
8394
logger.WithError(err).Error("Failed to get owner token.")
@@ -123,24 +134,35 @@ func (s *WorkspaceService) ListWorkspaces(ctx context.Context, req *connect.Requ
123134
}
124135

125136
func (s *WorkspaceService) UpdatePort(ctx context.Context, req *connect.Request[v1.UpdatePortRequest]) (*connect.Response[v1.UpdatePortResponse], error) {
137+
workspaceID, err := validateWorkspaceID(req.Msg.GetWorkspaceId())
138+
if err != nil {
139+
return nil, err
140+
}
141+
126142
conn, err := getConnection(ctx, s.connectionPool)
127143
if err != nil {
128144
return nil, err
129145
}
130-
if req.Msg.Port.Policy == v1.PortPolicy_PORT_POLICY_PRIVATE {
131-
_, err = conn.OpenPort(ctx, req.Msg.GetWorkspaceId(), &protocol.WorkspaceInstancePort{
146+
147+
switch req.Msg.GetPort().GetPolicy() {
148+
case v1.PortPolicy_PORT_POLICY_PRIVATE:
149+
_, err = conn.OpenPort(ctx, workspaceID, &protocol.WorkspaceInstancePort{
132150
Port: float64(req.Msg.Port.Port),
133151
Visibility: protocol.PortVisibilityPrivate,
134152
})
135-
} else if req.Msg.Port.Policy == v1.PortPolicy_PORT_POLICY_PUBLIC {
136-
_, err = conn.OpenPort(ctx, req.Msg.GetWorkspaceId(), &protocol.WorkspaceInstancePort{
153+
case v1.PortPolicy_PORT_POLICY_PUBLIC:
154+
_, err = conn.OpenPort(ctx, workspaceID, &protocol.WorkspaceInstancePort{
137155
Port: float64(req.Msg.Port.Port),
138156
Visibility: protocol.PortVisibilityPublic,
139157
})
158+
default:
159+
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Unknown port policy specified."))
140160
}
141161
if err != nil {
162+
log.WithField("workspace_id", workspaceID).Error("Failed to update port")
142163
return nil, proxy.ConvertError(err)
143164
}
165+
144166
return connect.NewResponse(
145167
&v1.UpdatePortResponse{},
146168
), nil
@@ -281,3 +303,16 @@ func parseGitpodTimestamp(input string) (*timestamppb.Timestamp, error) {
281303
}
282304
return timestamppb.New(parsed), nil
283305
}
306+
307+
func validateWorkspaceID(id string) (string, error) {
308+
if id == "" {
309+
return "", connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Empty workspace id specified"))
310+
}
311+
312+
err := namegen.ValidateWorkspaceID(id)
313+
if err != nil {
314+
return "", connect.NewError(connect.CodeInvalidArgument, err)
315+
}
316+
317+
return id, nil
318+
}

components/public-api-server/pkg/apiv1/workspace_test.go

Lines changed: 55 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package apiv1
66

77
import (
88
"context"
9+
"github.com/gitpod-io/gitpod/common-go/namegen"
910
"net/http"
1011
"net/http/httptest"
1112
"testing"
@@ -26,72 +27,60 @@ import (
2627
)
2728

2829
func TestWorkspaceService_GetWorkspace(t *testing.T) {
29-
const (
30-
bearerToken = "bearer-token-for-tests"
31-
foundWorkspaceID = "easycz-seer-xl8o1zacpyw"
32-
)
3330

34-
type Expectation struct {
35-
Code connect.Code
36-
Response *v1.GetWorkspaceResponse
37-
}
31+
workspaceID := workspaceTestData[0].Protocol.Workspace.ID
3832

39-
scenarios := []struct {
40-
name string
41-
WorkspaceID string
42-
Workspaces map[string]protocol.WorkspaceInfo
43-
Expect Expectation
44-
}{
45-
{
46-
name: "returns a workspace when workspace is found by ID",
47-
WorkspaceID: foundWorkspaceID,
48-
Workspaces: map[string]protocol.WorkspaceInfo{
49-
foundWorkspaceID: workspaceTestData[0].Protocol,
50-
},
51-
Expect: Expectation{
52-
Response: &v1.GetWorkspaceResponse{
53-
Result: workspaceTestData[0].API,
54-
},
55-
},
56-
},
57-
{
58-
name: "not found when workspace is not found by ID",
59-
WorkspaceID: "some-not-found-workspace-id",
60-
Expect: Expectation{
61-
Code: connect.CodeNotFound,
62-
},
63-
},
64-
}
33+
t.Run("invalid argument when workspace ID is missing", func(t *testing.T) {
34+
_, client := setupWorkspacesService(t)
6535

66-
for _, test := range scenarios {
67-
t.Run(test.name, func(t *testing.T) {
68-
serverMock, client := setupWorkspacesService(t)
36+
_, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
37+
WorkspaceId: "",
38+
}))
39+
require.Error(t, err)
40+
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
41+
})
6942

70-
serverMock.EXPECT().GetWorkspace(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id string) (res *protocol.WorkspaceInfo, err error) {
71-
w, ok := test.Workspaces[id]
72-
if !ok {
73-
return nil, &jsonrpc2.Error{
74-
Code: 404,
75-
Message: "not found",
76-
}
77-
}
78-
return &w, nil
79-
})
43+
t.Run("invalid argument when workspace ID does not validate", func(t *testing.T) {
44+
_, client := setupWorkspacesService(t)
8045

81-
resp, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
82-
WorkspaceId: test.WorkspaceID,
83-
}))
84-
requireErrorCode(t, test.Expect.Code, err)
85-
if test.Expect.Response != nil {
86-
requireEqualProto(t, test.Expect.Response, resp.Msg)
87-
}
46+
_, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
47+
WorkspaceId: "some-random-not-valid-workspace-id",
48+
}))
49+
require.Error(t, err)
50+
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
51+
})
52+
53+
t.Run("not found when workspace does not exist", func(t *testing.T) {
54+
serverMock, client := setupWorkspacesService(t)
55+
56+
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(nil, &jsonrpc2.Error{
57+
Code: 404,
58+
Message: "not found",
8859
})
89-
}
60+
61+
_, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
62+
WorkspaceId: workspaceID,
63+
}))
64+
require.Error(t, err)
65+
require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
66+
})
67+
68+
t.Run("returns a workspace when it exists", func(t *testing.T) {
69+
serverMock, client := setupWorkspacesService(t)
70+
71+
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&workspaceTestData[0].Protocol, nil)
72+
73+
resp, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
74+
WorkspaceId: workspaceID,
75+
}))
76+
require.NoError(t, err)
77+
78+
requireEqualProto(t, workspaceTestData[0].API, resp.Msg.GetResult())
79+
})
9080
}
9181

9282
func TestWorkspaceService_GetOwnerToken(t *testing.T) {
9383
const (
94-
bearerToken = "bearer-token-for-tests"
9584
foundWorkspaceID = "easycz-seer-xl8o1zacpyw"
9685
ownerToken = "some-owner-token"
9786
)
@@ -118,7 +107,7 @@ func TestWorkspaceService_GetOwnerToken(t *testing.T) {
118107
},
119108
{
120109
name: "not found when workspace is not found by ID",
121-
WorkspaceID: "some-not-found-workspace-id",
110+
WorkspaceID: mustGenerateWorkspaceID(t),
122111
Expect: Expectation{
123112
Code: connect.CodeNotFound,
124113
},
@@ -446,3 +435,12 @@ func requireErrorCode(t *testing.T, expected connect.Code, err error) {
446435
actual := connect.CodeOf(err)
447436
require.Equal(t, expected, actual, "expected code %s, but got %s from error %v", expected.String(), actual.String(), err)
448437
}
438+
439+
func mustGenerateWorkspaceID(t *testing.T) string {
440+
t.Helper()
441+
442+
wsid, err := namegen.GenerateWorkspaceID()
443+
require.NoError(t, err)
444+
445+
return wsid
446+
}

0 commit comments

Comments
 (0)