/
workspaceapps.go
210 lines (191 loc) · 6.77 KB
/
workspaceapps.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
203
204
205
206
207
208
209
210
package coderd
import (
"context"
"database/sql"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/apikey"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/workspaceapps"
"github.com/coder/coder/codersdk"
)
// @Summary Get applications host
// @ID get-applications-host
// @Security CoderSessionToken
// @Produce json
// @Tags Applications
// @Success 200 {object} codersdk.AppHostResponse
// @Router /applications/host [get]
// @Deprecated use api/v2/regions and see the primary proxy.
func (api *API) appHost(rw http.ResponseWriter, r *http.Request) {
host := api.AppHostname
if host != "" && api.AccessURL.Port() != "" {
host += fmt.Sprintf(":%s", api.AccessURL.Port())
}
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AppHostResponse{
Host: host,
})
}
// workspaceApplicationAuth is an endpoint on the main router that handles
// redirects from the subdomain handler.
//
// This endpoint is under /api so we don't return the friendly error page here.
// Any errors on this endpoint should be errors that are unlikely to happen
// in production unless the user messes with the URL.
//
// @Summary Redirect to URI with encrypted API key
// @ID redirect-to-uri-with-encrypted-api-key
// @Security CoderSessionToken
// @Tags Applications
// @Param redirect_uri query string false "Redirect destination"
// @Success 307
// @Router /applications/auth-redirect [get]
func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
if !api.Authorize(r, rbac.ActionCreate, apiKey) {
httpapi.ResourceNotFound(rw)
return
}
// Get the redirect URI from the query parameters and parse it.
redirectURI := r.URL.Query().Get(workspaceapps.RedirectURIQueryParam)
if redirectURI == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Missing redirect_uri query parameter.",
})
return
}
u, err := url.Parse(redirectURI)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid redirect_uri query parameter.",
Detail: err.Error(),
})
return
}
u.Scheme, err = api.ValidWorkspaceAppHostname(ctx, u.Host, ValidWorkspaceAppHostnameOpts{
// Allow all hosts except primary access URL since we don't need app
// tokens on the primary dashboard URL.
AllowPrimaryAccessURL: false,
AllowPrimaryWildcard: true,
AllowProxyAccessURL: true,
AllowProxyWildcard: true,
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to verify redirect_uri query parameter.",
Detail: err.Error(),
})
return
}
if u.Scheme == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid redirect_uri.",
Detail: "The redirect_uri query parameter must be the primary wildcard app hostname, a workspace proxy access URL or a workspace proxy wildcard app hostname.",
})
return
}
// Create the application_connect-scoped API key with the same lifetime as
// the current session.
exp := apiKey.ExpiresAt
lifetimeSeconds := apiKey.LifetimeSeconds
if exp.IsZero() || time.Until(exp) > api.DeploymentValues.SessionDuration.Value() {
exp = database.Now().Add(api.DeploymentValues.SessionDuration.Value())
lifetimeSeconds = int64(api.DeploymentValues.SessionDuration.Value().Seconds())
}
cookie, _, err := api.createAPIKey(ctx, apikey.CreateParams{
UserID: apiKey.UserID,
LoginType: database.LoginTypePassword,
DeploymentValues: api.DeploymentValues,
ExpiresAt: exp,
LifetimeSeconds: lifetimeSeconds,
Scope: database.APIKeyScopeApplicationConnect,
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to create API key.",
Detail: err.Error(),
})
return
}
// Encrypt the API key.
encryptedAPIKey, err := api.AppSecurityKey.EncryptAPIKey(workspaceapps.EncryptedAPIKeyPayload{
APIKey: cookie.Value,
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to encrypt API key.",
Detail: err.Error(),
})
return
}
// Redirect to the redirect URI with the encrypted API key in the query
// parameters.
q := u.Query()
q.Set(workspaceapps.SubdomainProxyAPIKeyParam, encryptedAPIKey)
u.RawQuery = q.Encode()
http.Redirect(rw, r, u.String(), http.StatusSeeOther)
}
type ValidWorkspaceAppHostnameOpts struct {
AllowPrimaryAccessURL bool
AllowPrimaryWildcard bool
AllowProxyAccessURL bool
AllowProxyWildcard bool
}
// ValidWorkspaceAppHostname checks if the given host is a valid workspace app
// hostname based on the provided options. It returns a scheme to force on
// success. If the hostname is not valid or doesn't match, an empty string is
// returned. Any error returned is a 500 error.
//
// For hosts that match a wildcard app hostname, the scheme is forced to be the
// corresponding access URL scheme.
func (api *API) ValidWorkspaceAppHostname(ctx context.Context, host string, opts ValidWorkspaceAppHostnameOpts) (string, error) {
if opts.AllowPrimaryAccessURL && (host == api.AccessURL.Hostname() || host == api.AccessURL.Host) {
// Force the redirect URI to have the same scheme as the access URL for
// security purposes.
return api.AccessURL.Scheme, nil
}
if opts.AllowPrimaryWildcard && api.AppHostnameRegex != nil {
_, ok := httpapi.ExecuteHostnamePattern(api.AppHostnameRegex, host)
if ok {
// Force the redirect URI to have the same scheme as the access URL
// for security purposes.
return api.AccessURL.Scheme, nil
}
}
// Ensure that the redirect URI is a subdomain of api.Hostname and is a
// valid app subdomain.
if opts.AllowProxyAccessURL || opts.AllowProxyWildcard {
// Strip the port for the database query.
host = strings.Split(host, ":")[0]
// nolint:gocritic // system query
systemCtx := dbauthz.AsSystemRestricted(ctx)
proxy, err := api.Database.GetWorkspaceProxyByHostname(systemCtx, database.GetWorkspaceProxyByHostnameParams{
Hostname: host,
AllowAccessUrl: opts.AllowProxyAccessURL,
AllowWildcardHostname: opts.AllowProxyWildcard,
})
if xerrors.Is(err, sql.ErrNoRows) {
return "", nil
}
if err != nil {
return "", xerrors.Errorf("get workspace proxy by hostname %q: %w", host, err)
}
proxyURL, err := url.Parse(proxy.Url)
if err != nil {
return "", xerrors.Errorf("parse proxy URL %q: %w", proxy.Url, err)
}
// Force the redirect URI to use the same scheme as the proxy access URL
// for security purposes.
return proxyURL.Scheme, nil
}
return "", nil
}