mirror of https://github.com/coder/coder.git
207 lines
6.8 KiB
Go
207 lines
6.8 KiB
Go
package coderd
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/apikey"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/coderd/httpmw"
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
|
"github.com/coder/coder/v2/coderd/workspaceapps"
|
|
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
|
"github.com/coder/coder/v2/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) {
|
|
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AppHostResponse{
|
|
Host: appurl.SubdomainAppHost(api.AppHostname, api.AccessURL),
|
|
})
|
|
}
|
|
|
|
// 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 = dbtime.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 := appurl.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
|
|
}
|