mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
d0db9ec88f
# Canonicalize API Key Scopes This PR introduces canonical API key scopes with a `coder:` namespace prefix to avoid collisions with low-level resource:action names. It: 1. Renames special API key scopes in the database: - `all` → `coder:all` - `application_connect` → `coder:application_connect` 2. Adds support for a new `scopes` field in the API key creation request, allowing multiple scopes to be specified while maintaining backward compatibility with the singular `scope` field. 3. Updates the API documentation to reflect these changes, including the new endpoint for listing public API key scopes. 4. Ensures backward compatibility by mapping between legacy and canonical scope names in relevant code paths.
209 lines
7.0 KiB
Go
209 lines
7.0 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/jwtutils"
|
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
"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, policy.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.Sessions.DefaultDuration.Value() {
|
|
exp = dbtime.Now().Add(api.DeploymentValues.Sessions.DefaultDuration.Value())
|
|
lifetimeSeconds = int64(api.DeploymentValues.Sessions.DefaultDuration.Value().Seconds())
|
|
}
|
|
cookie, _, err := api.createAPIKey(ctx, apikey.CreateParams{
|
|
UserID: apiKey.UserID,
|
|
LoginType: database.LoginTypePassword,
|
|
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
|
|
ExpiresAt: exp,
|
|
LifetimeSeconds: lifetimeSeconds,
|
|
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to create API key.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
payload := workspaceapps.EncryptedAPIKeyPayload{
|
|
APIKey: cookie.Value,
|
|
}
|
|
payload.Fill(api.Clock.Now())
|
|
encryptedAPIKey, err := jwtutils.Encrypt(ctx, api.AppEncryptionKeyCache, payload)
|
|
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
|
|
}
|