2022-01-13 22:55:28 +00:00
package coderd
import (
2022-11-16 22:34:06 +00:00
"context"
2022-10-17 13:43:30 +00:00
"crypto/tls"
2022-04-19 13:48:13 +00:00
"crypto/x509"
2022-11-16 22:34:06 +00:00
"encoding/json"
2022-10-25 00:46:24 +00:00
"fmt"
2022-06-13 18:14:22 +00:00
"io"
2022-01-13 22:55:28 +00:00
"net/http"
2022-02-28 18:26:01 +00:00
"net/url"
2022-06-21 16:53:36 +00:00
"path/filepath"
2022-10-14 18:25:11 +00:00
"regexp"
2023-01-18 19:12:53 +00:00
"strings"
2022-02-20 22:29:16 +00:00
"sync"
2022-09-20 04:11:01 +00:00
"sync/atomic"
2022-03-22 19:17:50 +00:00
"time"
2022-01-13 22:55:28 +00:00
2022-06-13 18:14:22 +00:00
"github.com/andybalholm/brotli"
2022-01-31 03:13:14 +00:00
"github.com/go-chi/chi/v5"
2022-05-03 12:48:02 +00:00
"github.com/go-chi/chi/v5/middleware"
2022-11-06 21:27:09 +00:00
"github.com/google/uuid"
2022-06-13 18:14:22 +00:00
"github.com/klauspost/compress/zstd"
2022-11-16 22:34:06 +00:00
"github.com/moby/moby/pkg/namesgenerator"
2022-08-08 15:09:46 +00:00
"github.com/prometheus/client_golang/prometheus"
2022-12-19 17:43:46 +00:00
httpSwagger "github.com/swaggo/http-swagger"
2022-09-16 16:43:22 +00:00
"go.opentelemetry.io/otel/trace"
2022-05-03 21:10:19 +00:00
"golang.org/x/xerrors"
2022-02-21 20:36:29 +00:00
"google.golang.org/api/idtoken"
2022-11-16 22:34:06 +00:00
"storj.io/drpc/drpcmux"
"storj.io/drpc/drpcserver"
2022-09-01 01:09:44 +00:00
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
2022-01-20 13:46:51 +00:00
2022-01-13 22:55:28 +00:00
"cdr.dev/slog"
2022-04-07 17:18:58 +00:00
"github.com/coder/coder/buildinfo"
2022-12-19 17:43:46 +00:00
// Used to serve the Swagger endpoint
_ "github.com/coder/coder/coderd/apidoc"
2022-09-20 04:11:01 +00:00
"github.com/coder/coder/coderd/audit"
2022-03-28 19:31:03 +00:00
"github.com/coder/coder/coderd/awsidentity"
2022-03-25 21:07:45 +00:00
"github.com/coder/coder/coderd/database"
2022-11-16 22:34:06 +00:00
"github.com/coder/coder/coderd/database/dbtype"
2022-10-25 00:46:24 +00:00
"github.com/coder/coder/coderd/gitauth"
2022-04-06 00:18:26 +00:00
"github.com/coder/coder/coderd/gitsshkey"
2022-03-25 21:07:45 +00:00
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
2022-09-01 19:58:23 +00:00
"github.com/coder/coder/coderd/metricscache"
2022-11-16 22:34:06 +00:00
"github.com/coder/coder/coderd/provisionerdserver"
2022-05-03 21:10:19 +00:00
"github.com/coder/coder/coderd/rbac"
2022-06-17 05:26:40 +00:00
"github.com/coder/coder/coderd/telemetry"
2022-05-19 22:43:07 +00:00
"github.com/coder/coder/coderd/tracing"
2022-12-01 17:43:28 +00:00
"github.com/coder/coder/coderd/updatecheck"
2023-01-18 19:12:53 +00:00
"github.com/coder/coder/coderd/util/slice"
2022-06-04 20:13:37 +00:00
"github.com/coder/coder/coderd/wsconncache"
2022-04-07 17:18:58 +00:00
"github.com/coder/coder/codersdk"
2022-11-14 17:57:33 +00:00
"github.com/coder/coder/provisionerd/proto"
2022-11-16 22:34:06 +00:00
"github.com/coder/coder/provisionersdk"
2022-01-18 21:13:19 +00:00
"github.com/coder/coder/site"
2022-09-01 01:09:44 +00:00
"github.com/coder/coder/tailnet"
2022-01-13 22:55:28 +00:00
)
2023-01-11 10:42:49 +00:00
// We must only ever instantiate one httpSwagger.Handler because of a data race
// inside the handler. This issue is triggered by tests that create multiple
// coderd instances.
//
// See https://github.com/swaggo/http-swagger/issues/78
var globalHTTPSwaggerHandler http . HandlerFunc
func init ( ) {
globalHTTPSwaggerHandler = httpSwagger . Handler ( httpSwagger . URL ( "/swagger/doc.json" ) )
}
2022-01-13 22:55:28 +00:00
// Options are requires parameters for Coder to start.
type Options struct {
2022-04-18 22:40:25 +00:00
AccessURL * url . URL
2022-09-22 22:30:32 +00:00
// AppHostname should be the wildcard hostname to use for workspace
2022-10-14 18:25:11 +00:00
// applications INCLUDING the asterisk, (optional) suffix and leading dot.
2022-12-15 18:43:00 +00:00
// It will use the same scheme and port number as the access URL.
2022-10-14 18:25:11 +00:00
// E.g. "*.apps.coder.com" or "*-apps.coder.com".
2022-09-22 22:30:32 +00:00
AppHostname string
2022-10-14 18:25:11 +00:00
// AppHostnameRegex contains the regex version of options.AppHostname as
// generated by httpapi.CompileHostnamePattern(). It MUST be set if
// options.AppHostname is set.
AppHostnameRegex * regexp . Regexp
Logger slog . Logger
Database database . Store
Pubsub database . Pubsub
2022-03-31 17:31:06 +00:00
2022-06-21 16:53:36 +00:00
// CacheDir is used for caching files served by the API.
CacheDir string
2022-09-20 04:11:01 +00:00
Auditor audit . Auditor
2022-04-18 22:40:25 +00:00
AgentConnectionUpdateFrequency time . Duration
2022-06-27 15:06:51 +00:00
AgentInactiveDisconnectTimeout time . Duration
2023-01-05 18:05:20 +00:00
AWSCertificates awsidentity . Certificates
Authorizer rbac . Authorizer
AzureCertificates x509 . VerifyOptions
GoogleTokenValidator * idtoken . Validator
GithubOAuth2Config * GithubOAuth2Config
OIDCConfig * OIDCConfig
PrometheusRegistry * prometheus . Registry
SecureAuthCookie bool
SSHKeygenAlgorithm gitsshkey . Algorithm
Telemetry telemetry . Reporter
TracerProvider trace . TracerProvider
GitAuthConfigs [ ] * gitauth . Config
RealIPConfig * httpmw . RealIPConfig
TrialGenerator func ( ctx context . Context , email string ) error
2022-10-17 13:43:30 +00:00
// TLSCertificates is used to mesh DERP servers securely.
TLSCertificates [ ] tls . Certificate
TailnetCoordinator tailnet . Coordinator
DERPServer * derp . Server
2022-09-01 01:09:44 +00:00
DERPMap * tailcfg . DERPMap
2022-12-19 17:43:46 +00:00
SwaggerEndpoint bool
2022-09-01 19:58:23 +00:00
2023-01-05 18:05:20 +00:00
// APIRateLimit is the minutely throughput rate limit per user or ip.
// Setting a rate limit <0 will disable the rate limiter across the entire
// app. Some specific routes have their own configurable rate limits.
APIRateLimit int
LoginRateLimit int
FilesRateLimit int
2022-09-01 19:58:23 +00:00
MetricsCacheRefreshInterval time . Duration
AgentStatsRefreshInterval time . Duration
2022-10-04 19:45:00 +00:00
Experimental bool
2022-10-21 22:08:23 +00:00
DeploymentConfig * codersdk . DeploymentConfig
2022-12-01 17:43:28 +00:00
UpdateCheckOptions * updatecheck . Options // Set non-nil to enable update checking.
2022-12-19 17:43:46 +00:00
HTTPClient * http . Client
2022-01-13 22:55:28 +00:00
}
2022-12-19 17:43:46 +00:00
// @title Coder API
// @version 2.0
// @description Coderd is the service created by running coder server. It is a thin API that connects workspaces, provisioners and users. coderd stores its state in Postgres and is the only service that communicates with Postgres.
// @termsOfService https://coder.com/legal/terms-of-service
// @contact.name API Support
// @contact.url https://coder.com
// @contact.email support@coder.com
// @license.name AGPL-3.0
// @license.url https://github.com/coder/coder/blob/main/LICENSE
// @BasePath /api/v2
// @securitydefinitions.apiKey CoderSessionToken
// @in header
// @name Coder-Session-Token
2022-05-26 03:14:08 +00:00
// New constructs a Coder API handler.
func New ( options * Options ) * API {
2022-09-20 04:11:01 +00:00
if options == nil {
options = & Options { }
}
2022-10-14 18:25:11 +00:00
if options . AppHostname != "" && options . AppHostnameRegex == nil || options . AppHostname == "" && options . AppHostnameRegex != nil {
panic ( "coderd: both AppHostname and AppHostnameRegex must be set or unset" )
}
2022-03-22 19:17:50 +00:00
if options . AgentConnectionUpdateFrequency == 0 {
options . AgentConnectionUpdateFrequency = 3 * time . Second
}
2022-06-27 15:06:51 +00:00
if options . AgentInactiveDisconnectTimeout == 0 {
// Multiply the update by two to allow for some lag-time.
options . AgentInactiveDisconnectTimeout = options . AgentConnectionUpdateFrequency * 2
}
2022-09-20 00:46:29 +00:00
if options . AgentStatsRefreshInterval == 0 {
2022-12-01 17:43:28 +00:00
options . AgentStatsRefreshInterval = 5 * time . Minute
2022-09-20 00:46:29 +00:00
}
if options . MetricsCacheRefreshInterval == 0 {
options . MetricsCacheRefreshInterval = time . Hour
}
2022-04-22 20:27:55 +00:00
if options . APIRateLimit == 0 {
options . APIRateLimit = 512
}
2023-01-05 18:05:20 +00:00
if options . LoginRateLimit == 0 {
options . LoginRateLimit = 60
}
if options . FilesRateLimit == 0 {
options . FilesRateLimit = 12
}
2022-11-05 00:03:01 +00:00
if options . PrometheusRegistry == nil {
options . PrometheusRegistry = prometheus . NewRegistry ( )
2022-08-08 15:09:46 +00:00
}
2023-01-13 22:07:15 +00:00
if options . Authorizer == nil {
options . Authorizer = rbac . NewAuthorizer ( options . PrometheusRegistry )
}
2022-09-01 01:09:44 +00:00
if options . TailnetCoordinator == nil {
options . TailnetCoordinator = tailnet . NewCoordinator ( )
}
2022-10-17 13:43:30 +00:00
if options . DERPServer == nil {
options . DERPServer = derp . NewServer ( key . NewNode ( ) , tailnet . Logger ( options . Logger . Named ( "derp" ) ) )
}
2022-09-20 04:11:01 +00:00
if options . Auditor == nil {
options . Auditor = audit . NewNop ( )
2022-08-29 23:45:40 +00:00
}
2022-05-26 03:14:08 +00:00
2022-06-21 16:53:36 +00:00
siteCacheDir := options . CacheDir
if siteCacheDir != "" {
siteCacheDir = filepath . Join ( siteCacheDir , "site" )
}
2023-01-17 18:38:08 +00:00
binFS , binHashes , err := site . ExtractOrReadBinFS ( siteCacheDir , site . FS ( ) )
2022-06-21 16:53:36 +00:00
if err != nil {
panic ( xerrors . Errorf ( "read site bin failed: %w" , err ) )
}
2022-09-01 19:58:23 +00:00
metricsCache := metricscache . New (
options . Database ,
options . Logger . Named ( "metrics_cache" ) ,
options . MetricsCacheRefreshInterval ,
)
2022-05-26 03:14:08 +00:00
r := chi . NewRouter ( )
api := & API {
2022-11-06 21:27:09 +00:00
ID : uuid . New ( ) ,
2022-06-04 20:13:37 +00:00
Options : options ,
2022-09-20 04:11:01 +00:00
RootHandler : r ,
2023-01-17 18:38:08 +00:00
siteHandler : site . Handler ( site . FS ( ) , binFS , binHashes ) ,
2022-09-20 04:11:01 +00:00
HTTPAuth : & HTTPAuthorizer {
2022-08-22 22:02:50 +00:00
Authorizer : options . Authorizer ,
Logger : options . Logger ,
} ,
2022-11-14 17:57:33 +00:00
metricsCache : metricsCache ,
Auditor : atomic . Pointer [ audit . Auditor ] { } ,
2023-01-18 19:12:53 +00:00
Experiments : initExperiments ( options . Logger , options . DeploymentConfig . Experiments . Value , options . DeploymentConfig . Experimental . Value ) ,
2022-05-26 03:14:08 +00:00
}
2022-12-01 17:43:28 +00:00
if options . UpdateCheckOptions != nil {
api . updateChecker = updatecheck . New (
options . Database ,
options . Logger . Named ( "update_checker" ) ,
* options . UpdateCheckOptions ,
)
}
2022-09-20 04:11:01 +00:00
api . Auditor . Store ( & options . Auditor )
2022-09-20 00:46:29 +00:00
api . workspaceAgentCache = wsconncache . New ( api . dialWorkspaceAgentTailnet , 0 )
2022-10-17 13:43:30 +00:00
api . TailnetCoordinator . Store ( & options . TailnetCoordinator )
2022-07-11 18:46:01 +00:00
oauthConfigs := & httpmw . OAuth2Configs {
2022-04-23 22:58:57 +00:00
Github : options . GithubOAuth2Config ,
2022-08-01 04:05:35 +00:00
OIDC : options . OIDCConfig ,
2022-07-11 18:46:01 +00:00
}
2022-09-22 22:30:32 +00:00
apiKeyMiddleware := httpmw . ExtractAPIKey ( httpmw . ExtractAPIKeyConfig {
DB : options . Database ,
OAuth2Configs : oauthConfigs ,
RedirectToLogin : false ,
Optional : false ,
} )
// Same as above but it redirects to the login page.
apiKeyMiddlewareRedirect := httpmw . ExtractAPIKey ( httpmw . ExtractAPIKeyConfig {
DB : options . Database ,
OAuth2Configs : oauthConfigs ,
RedirectToLogin : true ,
Optional : false ,
} )
2022-05-03 21:10:19 +00:00
2023-01-05 18:05:20 +00:00
// API rate limit middleware. The counter is local and not shared between
// replicas or instances of this middleware.
apiRateLimiter := httpmw . RateLimit ( options . APIRateLimit , time . Minute )
2022-05-03 12:48:02 +00:00
r . Use (
2022-08-29 22:00:52 +00:00
httpmw . Recover ( api . Logger ) ,
2022-11-28 21:22:10 +00:00
tracing . StatusWriterMiddleware ,
tracing . Middleware ( api . TracerProvider ) ,
httpmw . AttachRequestID ,
2022-10-23 18:21:49 +00:00
httpmw . ExtractRealIP ( api . RealIPConfig ) ,
2022-08-29 22:00:52 +00:00
httpmw . Logger ( api . Logger ) ,
2022-11-05 00:03:01 +00:00
httpmw . Prometheus ( options . PrometheusRegistry ) ,
2022-09-13 17:31:33 +00:00
// handleSubdomainApplications checks if the first subdomain is a valid
// app URL. If it is, it will serve that application.
api . handleSubdomainApplications (
2023-01-05 18:05:20 +00:00
apiRateLimiter ,
2022-09-13 17:31:33 +00:00
// Middleware to impose on the served application.
2022-09-22 22:30:32 +00:00
httpmw . ExtractAPIKey ( httpmw . ExtractAPIKeyConfig {
DB : options . Database ,
OAuth2Configs : oauthConfigs ,
// The code handles the the case where the user is not
// authenticated automatically.
RedirectToLogin : false ,
Optional : true ,
} ) ,
2022-10-14 16:46:38 +00:00
httpmw . ExtractUserParam ( api . Database , false ) ,
2022-09-13 17:31:33 +00:00
httpmw . ExtractWorkspaceAndAgentParam ( api . Database ) ,
) ,
2022-09-01 19:58:23 +00:00
// Build-Version is helpful for debugging.
func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
w . Header ( ) . Add ( "X-Coder-Build-Version" , buildinfo . Version ( ) )
2022-09-01 19:58:23 +00:00
next . ServeHTTP ( w , r )
} )
} ,
2022-09-13 19:26:46 +00:00
httpmw . CSRF ( options . SecureAuthCookie ) ,
2022-05-03 12:48:02 +00:00
)
2022-11-07 19:35:52 +00:00
r . Get ( "/healthz" , func ( w http . ResponseWriter , r * http . Request ) { _ , _ = w . Write ( [ ] byte ( "OK" ) ) } )
2022-06-04 20:13:37 +00:00
apps := func ( r chi . Router ) {
r . Use (
2023-01-05 18:05:20 +00:00
apiRateLimiter ,
2022-10-14 16:46:38 +00:00
httpmw . ExtractAPIKey ( httpmw . ExtractAPIKeyConfig {
DB : options . Database ,
OAuth2Configs : oauthConfigs ,
// Optional is true to allow for public apps. If an
// authorization check fails and the user is not authenticated,
// they will be redirected to the login page by the app handler.
RedirectToLogin : false ,
Optional : true ,
} ) ,
// Redirect to the login page if the user tries to open an app with
// "me" as the username and they are not logged in.
httpmw . ExtractUserParam ( api . Database , true ) ,
2022-08-29 12:56:52 +00:00
// Extracts the <workspace.agent> from the url
httpmw . ExtractWorkspaceAndAgentParam ( api . Database ) ,
2022-06-04 20:13:37 +00:00
)
2022-07-08 20:45:28 +00:00
r . HandleFunc ( "/*" , api . workspaceAppsProxyPath )
2022-06-04 20:13:37 +00:00
}
// %40 is the encoded character of the @ symbol. VS Code Web does
// not handle character encoding properly, so it's safe to assume
// other applications might not as well.
2022-08-29 12:56:52 +00:00
r . Route ( "/%40{user}/{workspace_and_agent}/apps/{workspaceapp}" , apps )
r . Route ( "/@{user}/{workspace_and_agent}/apps/{workspaceapp}" , apps )
2022-09-01 16:41:47 +00:00
r . Route ( "/derp" , func ( r chi . Router ) {
2022-10-17 13:43:30 +00:00
r . Get ( "/" , derphttp . Handler ( api . DERPServer ) . ServeHTTP )
2022-09-01 16:41:47 +00:00
// This is used when UDP is blocked, and latency must be checked via HTTP(s).
r . Get ( "/latency-check" , func ( w http . ResponseWriter , r * http . Request ) {
w . WriteHeader ( http . StatusOK )
} )
} )
2022-06-04 20:13:37 +00:00
2022-10-25 00:46:24 +00:00
r . Route ( "/gitauth" , func ( r chi . Router ) {
for _ , gitAuthConfig := range options . GitAuthConfigs {
r . Route ( fmt . Sprintf ( "/%s" , gitAuthConfig . ID ) , func ( r chi . Router ) {
r . Use (
2022-12-14 14:44:29 +00:00
httpmw . ExtractOAuth2 ( gitAuthConfig , options . HTTPClient ) ,
2022-10-25 00:46:24 +00:00
apiKeyMiddleware ,
)
r . Get ( "/callback" , api . gitAuthCallback ( gitAuthConfig ) )
} )
}
} )
2022-01-13 22:55:28 +00:00
r . Route ( "/api/v2" , func ( r chi . Router ) {
2022-09-20 04:11:01 +00:00
api . APIHandler = r
2022-09-20 20:16:26 +00:00
r . NotFound ( func ( rw http . ResponseWriter , r * http . Request ) { httpapi . RouteNotFound ( rw ) } )
2022-04-04 22:32:05 +00:00
r . Use (
2023-01-05 18:05:20 +00:00
// Specific routes can specify different limits, but every rate
// limit must be configurable by the admin.
apiRateLimiter ,
2022-04-04 22:32:05 +00:00
)
2022-12-22 14:53:14 +00:00
r . Get ( "/" , apiRoot )
2022-05-27 14:59:13 +00:00
// All CSP errors will be logged
r . Post ( "/csp/reports" , api . logReportCSPViolations )
2022-12-22 14:53:14 +00:00
r . Get ( "/buildinfo" , buildInfo )
2023-01-18 19:12:53 +00:00
r . Route ( "/experiments" , func ( r chi . Router ) {
r . Use ( apiKeyMiddleware )
r . Get ( "/" , api . handleExperimentsGet )
} )
2022-12-22 14:53:14 +00:00
r . Get ( "/updatecheck" , api . updateCheck )
2022-10-21 22:08:23 +00:00
r . Route ( "/config" , func ( r chi . Router ) {
2022-10-10 19:04:15 +00:00
r . Use ( apiKeyMiddleware )
2022-10-21 22:08:23 +00:00
r . Get ( "/deployment" , api . deploymentConfig )
2022-10-10 19:04:15 +00:00
} )
2022-09-07 16:38:19 +00:00
r . Route ( "/audit" , func ( r chi . Router ) {
r . Use (
apiKeyMiddleware ,
)
r . Get ( "/" , api . auditLogs )
r . Post ( "/testgenerate" , api . generateFakeAuditLog )
} )
2022-03-07 17:40:54 +00:00
r . Route ( "/files" , func ( r chi . Router ) {
2022-04-04 22:32:05 +00:00
r . Use (
2022-04-23 22:58:57 +00:00
apiKeyMiddleware ,
2023-01-05 18:05:20 +00:00
httpmw . RateLimit ( options . FilesRateLimit , time . Minute ) ,
2022-04-04 22:32:05 +00:00
)
2022-10-13 23:02:52 +00:00
r . Get ( "/{fileID}" , api . fileByID )
2022-05-26 03:14:08 +00:00
r . Post ( "/" , api . postFile )
2022-03-07 17:40:54 +00:00
} )
2022-05-27 16:19:13 +00:00
r . Route ( "/organizations" , func ( r chi . Router ) {
2022-01-25 19:52:58 +00:00
r . Use (
2022-04-23 22:58:57 +00:00
apiKeyMiddleware ,
2022-01-25 19:52:58 +00:00
)
2022-05-27 16:19:13 +00:00
r . Post ( "/" , api . postOrganizations )
r . Route ( "/{organization}" , func ( r chi . Router ) {
r . Use (
httpmw . ExtractOrganizationParam ( options . Database ) ,
)
r . Get ( "/" , api . organization )
2022-11-15 16:24:13 +00:00
r . Route ( "/templateversions" , func ( r chi . Router ) {
r . Post ( "/" , api . postTemplateVersionsByOrganization )
r . Get ( "/{templateversionname}" , api . templateVersionByOrganizationAndName )
2022-12-06 14:15:03 +00:00
r . Get ( "/{templateversionname}/previous" , api . previousTemplateVersionByOrganizationAndName )
2022-11-15 16:24:13 +00:00
} )
2022-05-27 16:19:13 +00:00
r . Route ( "/templates" , func ( r chi . Router ) {
r . Post ( "/" , api . postTemplateByOrganization )
r . Get ( "/" , api . templatesByOrganization )
r . Get ( "/{templatename}" , api . templateByOrganizationAndName )
2022-12-09 19:29:50 +00:00
r . Get ( "/examples" , api . templateExamples )
2022-04-25 21:11:03 +00:00
} )
2022-05-27 16:19:13 +00:00
r . Route ( "/members" , func ( r chi . Router ) {
r . Get ( "/roles" , api . assignableOrgRoles )
r . Route ( "/{user}" , func ( r chi . Router ) {
r . Use (
2022-10-14 16:46:38 +00:00
httpmw . ExtractUserParam ( options . Database , false ) ,
2022-05-27 16:19:13 +00:00
httpmw . ExtractOrganizationMemberParam ( options . Database ) ,
)
r . Put ( "/roles" , api . putMemberRoles )
2022-09-24 01:17:10 +00:00
r . Post ( "/workspaces" , api . postWorkspacesByOrganization )
2022-05-27 16:19:13 +00:00
} )
2022-04-29 14:04:19 +00:00
} )
} )
2022-03-07 17:40:54 +00:00
} )
r . Route ( "/parameters/{scope}/{id}" , func ( r chi . Router ) {
2022-05-31 13:06:42 +00:00
r . Use ( apiKeyMiddleware )
2022-05-26 03:14:08 +00:00
r . Post ( "/" , api . postParameter )
r . Get ( "/" , api . parameters )
2022-03-07 17:40:54 +00:00
r . Route ( "/{name}" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Delete ( "/" , api . deleteParameter )
2022-01-23 05:58:10 +00:00
} )
2022-01-20 13:46:51 +00:00
} )
2022-04-06 17:42:40 +00:00
r . Route ( "/templates/{template}" , func ( r chi . Router ) {
2022-01-24 17:07:42 +00:00
r . Use (
2022-04-23 22:58:57 +00:00
apiKeyMiddleware ,
2022-04-06 17:42:40 +00:00
httpmw . ExtractTemplateParam ( options . Database ) ,
2022-01-24 17:07:42 +00:00
)
2022-09-01 19:58:23 +00:00
r . Get ( "/daus" , api . templateDAUs )
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . template )
r . Delete ( "/" , api . deleteTemplate )
2022-06-08 14:14:57 +00:00
r . Patch ( "/" , api . patchTemplateMeta )
2022-03-07 17:40:54 +00:00
r . Route ( "/versions" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . templateVersionsByTemplate )
r . Patch ( "/" , api . patchActiveTemplateVersion )
r . Get ( "/{templateversionname}" , api . templateVersionByName )
2022-01-25 19:52:58 +00:00
} )
} )
2022-04-06 17:42:40 +00:00
r . Route ( "/templateversions/{templateversion}" , func ( r chi . Router ) {
2022-03-07 17:40:54 +00:00
r . Use (
2022-04-23 22:58:57 +00:00
apiKeyMiddleware ,
2022-04-06 17:42:40 +00:00
httpmw . ExtractTemplateVersionParam ( options . Database ) ,
2022-03-07 17:40:54 +00:00
)
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . templateVersion )
r . Patch ( "/cancel" , api . patchCancelTemplateVersion )
r . Get ( "/schema" , api . templateVersionSchema )
r . Get ( "/parameters" , api . templateVersionParameters )
2023-01-17 10:22:11 +00:00
r . Get ( "/rich-parameters" , api . templateVersionRichParameters )
2022-05-26 03:14:08 +00:00
r . Get ( "/resources" , api . templateVersionResources )
r . Get ( "/logs" , api . templateVersionLogs )
2022-06-01 14:44:53 +00:00
r . Route ( "/dry-run" , func ( r chi . Router ) {
r . Post ( "/" , api . postTemplateVersionDryRun )
r . Get ( "/{jobID}" , api . templateVersionDryRun )
r . Get ( "/{jobID}/resources" , api . templateVersionDryRunResources )
r . Get ( "/{jobID}/logs" , api . templateVersionDryRunLogs )
r . Patch ( "/{jobID}/cancel" , api . patchTemplateVersionDryRunCancel )
} )
2022-03-07 17:40:54 +00:00
} )
r . Route ( "/users" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Get ( "/first" , api . firstUser )
r . Post ( "/first" , api . postFirstUser )
2023-01-05 18:05:20 +00:00
r . Get ( "/authmethods" , api . userAuthMethods )
2022-10-20 17:01:23 +00:00
r . Group ( func ( r chi . Router ) {
2023-01-05 18:05:20 +00:00
// We use a tight limit for password login to protect against
// audit-log write DoS, pbkdf2 DoS, and simple brute-force
// attacks.
2022-10-20 17:01:23 +00:00
//
2023-01-05 18:05:20 +00:00
// This value is intentionally increased during tests.
r . Use ( httpmw . RateLimit ( options . LoginRateLimit , time . Minute ) )
2022-10-20 17:01:23 +00:00
r . Post ( "/login" , api . postLogin )
2023-01-05 18:05:20 +00:00
r . Route ( "/oauth2" , func ( r chi . Router ) {
r . Route ( "/github" , func ( r chi . Router ) {
r . Use ( httpmw . ExtractOAuth2 ( options . GithubOAuth2Config , options . HTTPClient ) )
r . Get ( "/callback" , api . userOAuth2Github )
} )
} )
r . Route ( "/oidc/callback" , func ( r chi . Router ) {
r . Use ( httpmw . ExtractOAuth2 ( options . OIDCConfig , options . HTTPClient ) )
r . Get ( "/" , api . userOIDC )
2022-04-23 22:58:57 +00:00
} )
2022-08-01 04:05:35 +00:00
} )
2022-03-07 17:40:54 +00:00
r . Group ( func ( r chi . Router ) {
2022-05-03 21:10:19 +00:00
r . Use (
apiKeyMiddleware ,
)
2022-05-26 03:14:08 +00:00
r . Post ( "/" , api . postUser )
r . Get ( "/" , api . users )
2022-05-27 20:47:03 +00:00
r . Post ( "/logout" , api . postLogout )
2022-05-03 21:10:19 +00:00
// These routes query information about site wide roles.
r . Route ( "/roles" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . assignableSiteRoles )
2022-05-03 21:10:19 +00:00
} )
2022-03-07 17:40:54 +00:00
r . Route ( "/{user}" , func ( r chi . Router ) {
2022-10-14 16:46:38 +00:00
r . Use ( httpmw . ExtractUserParam ( options . Database , false ) )
2022-09-12 23:24:20 +00:00
r . Delete ( "/" , api . deleteUser )
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . userByName )
r . Put ( "/profile" , api . putUserProfile )
2022-05-16 20:29:27 +00:00
r . Route ( "/status" , func ( r chi . Router ) {
2023-01-11 13:08:04 +00:00
r . Put ( "/suspend" , api . putSuspendUserAccount ( ) )
r . Put ( "/activate" , api . putActivateUserAccount ( ) )
2022-05-16 20:29:27 +00:00
} )
2022-05-06 14:20:08 +00:00
r . Route ( "/password" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Put ( "/" , api . putUserPassword )
2022-05-06 14:20:08 +00:00
} )
2022-05-03 21:10:19 +00:00
// These roles apply to the site wide permissions.
2022-05-26 03:14:08 +00:00
r . Put ( "/roles" , api . putUserRoles )
r . Get ( "/roles" , api . userRoles )
2022-05-03 21:10:19 +00:00
2022-06-27 18:50:52 +00:00
r . Route ( "/keys" , func ( r chi . Router ) {
2022-10-06 21:56:43 +00:00
r . Post ( "/" , api . postAPIKey )
2022-10-06 19:02:27 +00:00
r . Route ( "/tokens" , func ( r chi . Router ) {
r . Post ( "/" , api . postToken )
r . Get ( "/" , api . tokens )
} )
r . Route ( "/{keyid}" , func ( r chi . Router ) {
r . Get ( "/" , api . apiKey )
r . Delete ( "/" , api . deleteAPIKey )
} )
2022-06-27 18:50:52 +00:00
} )
2022-03-07 17:40:54 +00:00
r . Route ( "/organizations" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . organizationsByUser )
r . Get ( "/{organizationname}" , api . organizationByUserAndName )
2022-03-07 17:40:54 +00:00
} )
2022-06-10 16:08:50 +00:00
r . Route ( "/workspace/{workspacename}" , func ( r chi . Router ) {
r . Get ( "/" , api . workspaceByOwnerAndName )
r . Get ( "/builds/{buildnumber}" , api . workspaceBuildByBuildNumber )
} )
2022-05-26 03:14:08 +00:00
r . Get ( "/gitsshkey" , api . gitSSHKey )
r . Put ( "/gitsshkey" , api . regenerateGitSSHKey )
2022-01-24 17:07:42 +00:00
} )
} )
} )
2022-04-11 21:06:15 +00:00
r . Route ( "/workspaceagents" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Post ( "/azure-instance-identity" , api . postWorkspaceAuthAzureInstanceIdentity )
r . Post ( "/aws-instance-identity" , api . postWorkspaceAuthAWSInstanceIdentity )
r . Post ( "/google-instance-identity" , api . postWorkspaceAuthGoogleInstanceIdentity )
2022-04-11 21:06:15 +00:00
r . Route ( "/me" , func ( r chi . Router ) {
2022-03-07 17:40:54 +00:00
r . Use ( httpmw . ExtractWorkspaceAgent ( options . Database ) )
2022-05-26 03:14:08 +00:00
r . Get ( "/metadata" , api . workspaceAgentMetadata )
2022-08-31 15:33:50 +00:00
r . Post ( "/version" , api . postWorkspaceAgentVersion )
2022-09-23 19:51:04 +00:00
r . Post ( "/app-health" , api . postWorkspaceAppHealth )
2022-10-25 00:46:24 +00:00
r . Get ( "/gitauth" , api . workspaceAgentsGitAuth )
2022-05-26 03:14:08 +00:00
r . Get ( "/gitsshkey" , api . agentGitSSHKey )
2022-09-01 01:09:44 +00:00
r . Get ( "/coordinate" , api . workspaceAgentCoordinate )
2022-11-18 22:46:53 +00:00
r . Post ( "/report-stats" , api . workspaceAgentReportStats )
2023-01-24 12:24:27 +00:00
r . Post ( "/report-lifecycle" , api . workspaceAgentReportLifecycle )
2022-03-07 17:40:54 +00:00
} )
2022-04-11 21:06:15 +00:00
r . Route ( "/{workspaceagent}" , func ( r chi . Router ) {
2022-03-07 17:40:54 +00:00
r . Use (
2022-04-23 22:58:57 +00:00
apiKeyMiddleware ,
2022-04-11 21:06:15 +00:00
httpmw . ExtractWorkspaceAgentParam ( options . Database ) ,
2022-06-10 14:46:48 +00:00
httpmw . ExtractWorkspaceParam ( options . Database ) ,
2022-03-07 17:40:54 +00:00
)
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . workspaceAgent )
r . Get ( "/pty" , api . workspaceAgentPTY )
2022-10-06 12:38:22 +00:00
r . Get ( "/listening-ports" , api . workspaceAgentListeningPorts )
2022-09-01 01:09:44 +00:00
r . Get ( "/connection" , api . workspaceAgentConnection )
r . Get ( "/coordinate" , api . workspaceAgentClientCoordinate )
2022-02-21 20:36:29 +00:00
} )
} )
2022-05-18 15:09:07 +00:00
r . Route ( "/workspaces" , func ( r chi . Router ) {
2022-02-12 19:34:04 +00:00
r . Use (
2022-04-23 22:58:57 +00:00
apiKeyMiddleware ,
2022-02-12 19:34:04 +00:00
)
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . workspaces )
2022-05-18 15:09:07 +00:00
r . Route ( "/{workspace}" , func ( r chi . Router ) {
r . Use (
httpmw . ExtractWorkspaceParam ( options . Database ) ,
)
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . workspace )
2022-08-26 09:28:38 +00:00
r . Patch ( "/" , api . patchWorkspace )
2022-05-18 15:09:07 +00:00
r . Route ( "/builds" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . workspaceBuilds )
r . Post ( "/" , api . postWorkspaceBuilds )
2022-05-18 15:09:07 +00:00
} )
r . Route ( "/autostart" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Put ( "/" , api . putWorkspaceAutostart )
2022-05-18 15:09:07 +00:00
} )
2022-05-19 19:09:27 +00:00
r . Route ( "/ttl" , func ( r chi . Router ) {
2022-05-26 03:14:08 +00:00
r . Put ( "/" , api . putWorkspaceTTL )
2022-05-18 15:09:07 +00:00
} )
2022-05-26 03:14:08 +00:00
r . Get ( "/watch" , api . watchWorkspace )
2022-05-26 17:08:11 +00:00
r . Put ( "/extend" , api . putExtendWorkspace )
2022-04-07 09:03:35 +00:00
} )
2022-02-12 19:34:04 +00:00
} )
2022-03-07 17:40:54 +00:00
r . Route ( "/workspacebuilds/{workspacebuild}" , func ( r chi . Router ) {
2022-02-12 19:34:04 +00:00
r . Use (
2022-04-23 22:58:57 +00:00
apiKeyMiddleware ,
2022-03-07 17:40:54 +00:00
httpmw . ExtractWorkspaceBuildParam ( options . Database ) ,
httpmw . ExtractWorkspaceParam ( options . Database ) ,
2022-02-12 19:34:04 +00:00
)
2022-05-26 03:14:08 +00:00
r . Get ( "/" , api . workspaceBuild )
r . Patch ( "/cancel" , api . patchCancelWorkspaceBuild )
r . Get ( "/logs" , api . workspaceBuildLogs )
2023-01-17 15:24:45 +00:00
r . Get ( "/parameters" , api . workspaceBuildParameters )
2022-05-26 03:14:08 +00:00
r . Get ( "/resources" , api . workspaceBuildResources )
r . Get ( "/state" , api . workspaceBuildState )
2022-02-12 19:34:04 +00:00
} )
2022-09-22 22:30:32 +00:00
r . Route ( "/authcheck" , func ( r chi . Router ) {
r . Use ( apiKeyMiddleware )
r . Post ( "/" , api . checkAuthorization )
} )
r . Route ( "/applications" , func ( r chi . Router ) {
r . Route ( "/host" , func ( r chi . Router ) {
// Don't leak the hostname to unauthenticated users.
r . Use ( apiKeyMiddleware )
r . Get ( "/" , api . appHost )
} )
r . Route ( "/auth-redirect" , func ( r chi . Router ) {
// We want to redirect to login if they are not authenticated.
r . Use ( apiKeyMiddlewareRedirect )
// This is a GET request as it's redirected to by the subdomain app
// handler and the login page.
r . Get ( "/" , api . workspaceApplicationAuth )
} )
} )
2023-01-25 21:27:36 +00:00
r . Route ( "/debug" , func ( r chi . Router ) {
r . Use (
apiKeyMiddleware ,
// Ensure only owners can access debug endpoints.
func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( rw http . ResponseWriter , r * http . Request ) {
if ! api . Authorize ( r , rbac . ActionRead , rbac . ResourceDebugInfo ) {
httpapi . ResourceNotFound ( rw )
return
}
next . ServeHTTP ( rw , r )
} )
} ,
)
r . Get ( "/coordinator" , api . debugCoordinator )
} )
2022-01-13 22:55:28 +00:00
} )
2022-06-13 18:14:22 +00:00
2022-12-19 17:43:46 +00:00
if options . SwaggerEndpoint {
// Swagger UI requires the URL trailing slash. Otherwise, the browser tries to load /assets
// from http://localhost:8080/assets instead of http://localhost:8080/swagger/assets.
r . Get ( "/swagger" , http . RedirectHandler ( "/swagger/" , http . StatusTemporaryRedirect ) . ServeHTTP )
2023-01-11 10:42:49 +00:00
// See globalHTTPSwaggerHandler comment as to why we use a package
// global variable here.
r . Get ( "/swagger/*" , globalHTTPSwaggerHandler )
2022-12-19 17:43:46 +00:00
}
2022-06-13 18:14:22 +00:00
r . NotFound ( compressHandler ( http . HandlerFunc ( api . siteHandler . ServeHTTP ) ) . ServeHTTP )
2022-05-26 03:14:08 +00:00
return api
2022-01-13 22:55:28 +00:00
}
2022-02-01 22:15:26 +00:00
2022-05-26 03:14:08 +00:00
type API struct {
2022-02-20 22:29:16 +00:00
* Options
2022-11-06 21:27:09 +00:00
// ID is a uniquely generated ID on initialization.
// This is used to associate objects with a specific
// Coder API instance, like workspace agents to a
// specific replica.
ID uuid . UUID
2022-09-22 15:14:22 +00:00
Auditor atomic . Pointer [ audit . Auditor ]
WorkspaceClientCoordinateOverride atomic . Pointer [ func ( rw http . ResponseWriter ) bool ]
2022-10-17 13:43:30 +00:00
TailnetCoordinator atomic . Pointer [ tailnet . Coordinator ]
2022-11-14 17:57:33 +00:00
QuotaCommitter atomic . Pointer [ proto . QuotaCommitter ]
2022-09-22 15:14:22 +00:00
HTTPAuth * HTTPAuthorizer
2022-02-20 22:29:16 +00:00
2022-09-20 04:11:01 +00:00
// APIHandler serves "/api/v2"
APIHandler chi . Router
// RootHandler serves "/"
RootHandler chi . Router
2022-09-01 01:09:44 +00:00
2022-12-01 17:43:28 +00:00
siteHandler http . Handler
2022-11-16 22:34:06 +00:00
WebsocketWaitMutex sync . Mutex
WebsocketWaitGroup sync . WaitGroup
2022-12-01 17:43:28 +00:00
metricsCache * metricscache . Cache
2022-06-04 20:13:37 +00:00
workspaceAgentCache * wsconncache . Cache
2022-12-01 17:43:28 +00:00
updateChecker * updatecheck . Checker
2023-01-18 19:12:53 +00:00
// Experiments contains the list of experiments currently enabled.
// This is used to gate features that are not yet ready for production.
Experiments codersdk . Experiments
2022-02-01 22:15:26 +00:00
}
2022-04-08 14:35:29 +00:00
2022-05-26 03:14:08 +00:00
// Close waits for all WebSocket connections to drain before returning.
2022-06-04 20:13:37 +00:00
func ( api * API ) Close ( ) error {
2022-11-16 22:34:06 +00:00
api . WebsocketWaitMutex . Lock ( )
api . WebsocketWaitGroup . Wait ( )
api . WebsocketWaitMutex . Unlock ( )
2022-06-04 20:13:37 +00:00
2022-09-01 19:58:23 +00:00
api . metricsCache . Close ( )
2022-12-01 17:43:28 +00:00
if api . updateChecker != nil {
api . updateChecker . Close ( )
}
2022-10-17 13:43:30 +00:00
coordinator := api . TailnetCoordinator . Load ( )
if coordinator != nil {
_ = ( * coordinator ) . Close ( )
}
2022-06-04 20:13:37 +00:00
return api . workspaceAgentCache . Close ( )
2022-05-26 03:14:08 +00:00
}
2022-06-13 18:14:22 +00:00
func compressHandler ( h http . Handler ) http . Handler {
cmp := middleware . NewCompressor ( 5 ,
"text/*" ,
"application/*" ,
"image/*" ,
)
cmp . SetEncoder ( "br" , func ( w io . Writer , level int ) io . Writer {
return brotli . NewWriterLevel ( w , level )
} )
cmp . SetEncoder ( "zstd" , func ( w io . Writer , level int ) io . Writer {
zw , err := zstd . NewWriter ( w , zstd . WithEncoderLevel ( zstd . EncoderLevelFromZstd ( level ) ) )
if err != nil {
panic ( "invalid zstd compressor: " + err . Error ( ) )
}
return zw
} )
return cmp . Handler ( h )
}
2022-11-16 22:34:06 +00:00
2022-12-01 22:54:53 +00:00
// CreateInMemoryProvisionerDaemon is an in-memory connection to a provisionerd.
// Useful when starting coderd and provisionerd in the same process.
2022-11-16 22:34:06 +00:00
func ( api * API ) CreateInMemoryProvisionerDaemon ( ctx context . Context , debounce time . Duration ) ( client proto . DRPCProvisionerDaemonClient , err error ) {
2022-11-22 18:19:32 +00:00
clientSession , serverSession := provisionersdk . MemTransportPipe ( )
2022-11-16 22:34:06 +00:00
defer func ( ) {
if err != nil {
_ = clientSession . Close ( )
_ = serverSession . Close ( )
}
} ( )
name := namesgenerator . GetRandomName ( 1 )
daemon , err := api . Database . InsertProvisionerDaemon ( ctx , database . InsertProvisionerDaemonParams {
ID : uuid . New ( ) ,
CreatedAt : database . Now ( ) ,
Name : name ,
Provisioners : [ ] database . ProvisionerType { database . ProvisionerTypeEcho , database . ProvisionerTypeTerraform } ,
Tags : dbtype . StringMap {
provisionerdserver . TagScope : provisionerdserver . ScopeOrganization ,
} ,
} )
if err != nil {
return nil , xerrors . Errorf ( "insert provisioner daemon %q: %w" , name , err )
}
tags , err := json . Marshal ( daemon . Tags )
if err != nil {
return nil , xerrors . Errorf ( "marshal tags: %w" , err )
}
mux := drpcmux . New ( )
err = proto . DRPCRegisterProvisionerDaemon ( mux , & provisionerdserver . Server {
AccessURL : api . AccessURL ,
ID : daemon . ID ,
Database : api . Database ,
Pubsub : api . Pubsub ,
Provisioners : daemon . Provisioners ,
Telemetry : api . Telemetry ,
Tags : tags ,
QuotaCommitter : & api . QuotaCommitter ,
2022-11-22 18:22:56 +00:00
Auditor : & api . Auditor ,
2022-11-16 22:34:06 +00:00
AcquireJobDebounce : debounce ,
Logger : api . Logger . Named ( fmt . Sprintf ( "provisionerd-%s" , daemon . Name ) ) ,
} )
if err != nil {
return nil , err
}
server := drpcserver . NewWithOptions ( mux , drpcserver . Options {
Log : func ( err error ) {
if xerrors . Is ( err , io . EOF ) {
return
}
api . Logger . Debug ( ctx , "drpc server error" , slog . Error ( err ) )
} ,
} )
go func ( ) {
err := server . Serve ( ctx , serverSession )
if err != nil && ! xerrors . Is ( err , io . EOF ) {
api . Logger . Debug ( ctx , "provisioner daemon disconnected" , slog . Error ( err ) )
}
// close the sessions so we don't leak goroutines serving them.
_ = clientSession . Close ( )
_ = serverSession . Close ( )
} ( )
2022-11-22 18:19:32 +00:00
return proto . NewDRPCProvisionerDaemonClient ( clientSession ) , nil
2022-11-16 22:34:06 +00:00
}
2023-01-18 19:12:53 +00:00
// nolint:revive
func initExperiments ( log slog . Logger , raw [ ] string , legacyAll bool ) codersdk . Experiments {
exps := make ( [ ] codersdk . Experiment , 0 , len ( raw ) )
for _ , v := range raw {
switch v {
case "*" :
exps = append ( exps , codersdk . ExperimentsAll ... )
default :
ex := codersdk . Experiment ( strings . ToLower ( v ) )
if ! slice . Contains ( codersdk . ExperimentsAll , ex ) {
log . Warn ( context . Background ( ) , "🐉 HERE BE DRAGONS: opting into hidden experiment" , slog . F ( "experiment" , ex ) )
}
exps = append ( exps , ex )
}
}
// --experiments takes precedence over --experimental. It's deprecated.
if legacyAll && len ( raw ) == 0 {
log . Warn ( context . Background ( ) , "--experimental is deprecated, use --experiments='*' instead" )
exps = append ( exps , codersdk . ExperimentsAll ... )
}
return exps
}