2022-06-04 20:13:37 +00:00
package coderd
import (
2023-04-20 23:59:45 +00:00
"context"
2023-04-17 19:57:21 +00:00
"database/sql"
2022-06-04 20:13:37 +00:00
"net/http"
"net/url"
2023-04-20 23:59:45 +00:00
"strings"
2022-09-22 22:30:32 +00:00
"time"
2022-06-04 20:13:37 +00:00
2023-04-17 19:57:21 +00:00
"golang.org/x/xerrors"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/apikey"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
2023-09-01 16:50:12 +00:00
"github.com/coder/coder/v2/coderd/database/dbtime"
2023-08-18 18:55:43 +00:00
"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"
2024-01-17 16:41:42 +00:00
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/codersdk"
2022-09-22 22:30:32 +00:00
)
2022-12-21 14:37:30 +00:00
// @Summary Get applications host
2023-01-13 11:27:21 +00:00
// @ID get-applications-host
2022-12-21 14:37:30 +00:00
// @Security CoderSessionToken
// @Produce json
// @Tags Applications
2023-01-29 21:47:24 +00:00
// @Success 200 {object} codersdk.AppHostResponse
2022-12-21 14:37:30 +00:00
// @Router /applications/host [get]
2023-05-09 14:28:25 +00:00
// @Deprecated use api/v2/regions and see the primary proxy.
2022-09-22 22:30:32 +00:00
func ( api * API ) appHost ( rw http . ResponseWriter , r * http . Request ) {
2023-01-29 21:47:24 +00:00
httpapi . Write ( r . Context ( ) , rw , http . StatusOK , codersdk . AppHostResponse {
2024-01-18 15:44:05 +00:00
Host : appurl . SubdomainAppHost ( api . AppHostname , api . AccessURL ) ,
2022-09-22 22:30:32 +00:00
} )
}
2023-03-07 19:38:11 +00:00
// 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
}
2022-10-14 16:46:38 +00:00
2023-03-07 19:38:11 +00:00
// 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
}
2022-09-22 22:30:32 +00:00
2023-04-20 23:59:45 +00:00
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
2023-04-17 19:57:21 +00:00
}
2023-04-20 23:59:45 +00:00
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
2023-03-07 19:38:11 +00:00
}
// Create the application_connect-scoped API key with the same lifetime as
// the current session.
exp := apiKey . ExpiresAt
lifetimeSeconds := apiKey . LifetimeSeconds
2024-04-10 15:34:49 +00:00
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 ( ) )
2023-03-07 19:38:11 +00:00
}
2023-05-18 04:29:22 +00:00
cookie , _ , err := api . createAPIKey ( ctx , apikey . CreateParams {
2024-01-22 20:42:55 +00:00
UserID : apiKey . UserID ,
LoginType : database . LoginTypePassword ,
2024-04-10 15:34:49 +00:00
DefaultLifetime : api . DeploymentValues . Sessions . DefaultDuration . Value ( ) ,
2024-01-22 20:42:55 +00:00
ExpiresAt : exp ,
LifetimeSeconds : lifetimeSeconds ,
Scope : database . APIKeyScopeApplicationConnect ,
2023-03-07 19:38:11 +00:00
} )
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.
2023-04-05 18:41:55 +00:00
encryptedAPIKey , err := api . AppSecurityKey . EncryptAPIKey ( workspaceapps . EncryptedAPIKeyPayload {
2023-03-07 19:38:11 +00:00
APIKey : cookie . Value ,
} )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to encrypt API key." ,
Detail : err . Error ( ) ,
2022-06-04 20:13:37 +00:00
} )
2023-03-07 19:38:11 +00:00
return
2022-09-13 17:31:33 +00:00
}
2023-03-07 19:38:11 +00:00
// Redirect to the redirect URI with the encrypted API key in the query
// parameters.
q := u . Query ( )
2023-04-05 18:41:55 +00:00
q . Set ( workspaceapps . SubdomainProxyAPIKeyParam , encryptedAPIKey )
2023-03-07 19:38:11 +00:00
u . RawQuery = q . Encode ( )
2023-04-17 19:57:21 +00:00
http . Redirect ( rw , r , u . String ( ) , http . StatusSeeOther )
2022-09-13 17:31:33 +00:00
}
2023-04-20 23:59:45 +00:00
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 {
2024-01-17 16:41:42 +00:00
_ , ok := appurl . ExecuteHostnamePattern ( api . AppHostnameRegex , host )
2023-04-20 23:59:45 +00:00
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
}