-
Notifications
You must be signed in to change notification settings - Fork 569
/
workspaceproxy.go
202 lines (181 loc) · 6.14 KB
/
workspaceproxy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
package httpmw
import (
"context"
"crypto/sha256"
"crypto/subtle"
"database/sql"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/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)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
type workspaceProxyParamContextKey struct{}
// WorkspaceProxyParam returns the worksace proxy from the ExtractWorkspaceProxyParam handler.
func WorkspaceProxyParam(r *http.Request) database.WorkspaceProxy {
user, ok := r.Context().Value(workspaceProxyParamContextKey{}).(database.WorkspaceProxy)
if !ok {
panic("developer error: workspace proxy parameter middleware not provided")
}
return user
}
// ExtractWorkspaceProxyParam extracts a workspace proxy from an ID/name in the {workspaceproxy} URL
// parameter.
//
//nolint:revive
func ExtractWorkspaceProxyParam(db database.Store, deploymentID string, fetchPrimaryProxy func(ctx context.Context) (database.WorkspaceProxy, error)) 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()
proxyQuery := chi.URLParam(r, "workspaceproxy")
if proxyQuery == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "\"workspaceproxy\" must be provided.",
})
return
}
var proxy database.WorkspaceProxy
var dbErr error
if proxyQuery == "primary" || proxyQuery == deploymentID {
// Requesting primary proxy
proxy, dbErr = fetchPrimaryProxy(ctx)
} else if proxyID, err := uuid.Parse(proxyQuery); err == nil {
// Request proxy by id
proxy, dbErr = db.GetWorkspaceProxyByID(ctx, proxyID)
} else {
// Request proxy by name
proxy, dbErr = db.GetWorkspaceProxyByName(ctx, proxyQuery)
}
if httpapi.Is404Error(dbErr) {
httpapi.ResourceNotFound(rw)
return
}
if dbErr != nil {
httpapi.InternalServerError(rw, dbErr)
return
}
ctx = context.WithValue(ctx, workspaceProxyParamContextKey{}, proxy)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}