2022-08-22 22:02:50 +00:00
package coderd
import (
2022-08-29 23:45:40 +00:00
"context"
2022-09-20 04:11:01 +00:00
"crypto/ed25519"
2023-07-26 16:21:04 +00:00
"fmt"
"math"
2022-09-20 04:11:01 +00:00
"net/http"
2023-07-26 16:21:04 +00:00
"net/url"
"strconv"
"strings"
2022-09-20 04:11:01 +00:00
"sync"
"time"
2022-08-29 23:45:40 +00:00
2024-01-29 08:17:31 +00:00
"github.com/coder/coder/v2/coderd/appearance"
2024-02-13 14:31:20 +00:00
agplportsharing "github.com/coder/coder/v2/coderd/portsharing"
"github.com/coder/coder/v2/enterprise/coderd/portsharing"
2024-01-29 08:17:31 +00:00
2022-08-22 22:02:50 +00:00
"golang.org/x/xerrors"
2023-07-26 16:21:04 +00:00
"tailscale.com/tailcfg"
2022-08-22 22:02:50 +00:00
2022-09-20 04:11:01 +00:00
"github.com/cenkalti/backoff/v4"
"github.com/go-chi/chi/v5"
2023-01-13 22:07:15 +00:00
"github.com/prometheus/client_golang/prometheus"
2022-09-20 04:11:01 +00:00
"cdr.dev/slog"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd"
agplaudit "github.com/coder/coder/v2/coderd/audit"
2023-10-18 22:07:21 +00:00
agpldbauthz "github.com/coder/coder/v2/coderd/database/dbauthz"
2023-11-24 15:06:51 +00:00
"github.com/coder/coder/v2/coderd/healthcheck"
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"
agplschedule "github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/codersdk"
2023-10-18 22:07:21 +00:00
"github.com/coder/coder/v2/enterprise/coderd/dbauthz"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/enterprise/coderd/proxyhealth"
"github.com/coder/coder/v2/enterprise/coderd/schedule"
2023-09-07 14:49:49 +00:00
"github.com/coder/coder/v2/enterprise/dbcrypt"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/enterprise/derpmesh"
"github.com/coder/coder/v2/enterprise/replicasync"
"github.com/coder/coder/v2/enterprise/tailnet"
"github.com/coder/coder/v2/provisionerd/proto"
agpltailnet "github.com/coder/coder/v2/tailnet"
2022-08-22 22:02:50 +00:00
)
2022-09-20 04:11:01 +00:00
// New constructs an Enterprise coderd API instance.
// This handler is designed to wrap the AGPL Coder code and
// layer Enterprise functionality on top as much as possible.
2023-06-13 17:18:31 +00:00
func New ( ctx context . Context , options * Options ) ( _ * API , err error ) {
2022-09-20 04:11:01 +00:00
if options . EntitlementsUpdateInterval == 0 {
options . EntitlementsUpdateInterval = 10 * time . Minute
}
2023-09-07 14:49:49 +00:00
if options . LicenseKeys == nil {
options . LicenseKeys = Keys
2022-09-20 04:11:01 +00:00
}
2022-10-14 16:46:38 +00:00
if options . Options == nil {
options . Options = & coderd . Options { }
}
2023-01-13 22:07:15 +00:00
if options . PrometheusRegistry == nil {
options . PrometheusRegistry = prometheus . NewRegistry ( )
}
2022-10-14 16:46:38 +00:00
if options . Options . Authorizer == nil {
2023-02-15 14:56:07 +00:00
options . Options . Authorizer = rbac . NewCachingAuthorizer ( options . PrometheusRegistry )
2022-10-14 16:46:38 +00:00
}
2023-07-20 13:35:41 +00:00
2022-09-20 04:11:01 +00:00
ctx , cancelFunc := context . WithCancel ( ctx )
2023-04-24 15:25:35 +00:00
2023-09-07 14:49:49 +00:00
if options . ExternalTokenEncryption == nil {
options . ExternalTokenEncryption = make ( [ ] dbcrypt . Cipher , 0 )
}
// Database encryption is an enterprise feature, but as checking license entitlements
// depends on the database, we end up in a chicken-and-egg situation. To avoid this,
// we always enable it but only soft-enforce it.
if len ( options . ExternalTokenEncryption ) > 0 {
var keyDigests [ ] string
for _ , cipher := range options . ExternalTokenEncryption {
keyDigests = append ( keyDigests , cipher . HexDigest ( ) )
}
options . Logger . Info ( ctx , "database encryption enabled" , slog . F ( "keys" , keyDigests ) )
}
cryptDB , err := dbcrypt . New ( ctx , options . Database , options . ExternalTokenEncryption ... )
if err != nil {
cancelFunc ( )
// If we fail to initialize the database, it's likely that the
// database is encrypted with an unknown external token encryption key.
// This is a fatal error.
var derr * dbcrypt . DecryptFailedError
if xerrors . As ( err , & derr ) {
return nil , xerrors . Errorf ( "database encrypted with unknown key, either add the key or see https://coder.com/docs/v2/latest/admin/encryption#disabling-encryption: %w" , derr )
}
return nil , xerrors . Errorf ( "init database encryption: %w" , err )
}
options . Database = cryptDB
api := & API {
ctx : ctx ,
cancel : cancelFunc ,
2023-04-24 15:25:35 +00:00
Options : options ,
2023-08-04 08:32:28 +00:00
provisionerDaemonAuth : & provisionerDaemonAuth {
psk : options . ProvisionerDaemonPSK ,
authorizer : options . Authorizer ,
} ,
2022-09-20 04:11:01 +00:00
}
2024-03-25 19:01:42 +00:00
// This must happen before coderd initialization!
options . PostAuthAdditionalHeadersFunc = api . writeEntitlementWarningsHeader
api . AGPL = coderd . New ( options . Options )
2023-06-13 17:18:31 +00:00
defer func ( ) {
if err != nil {
_ = api . Close ( )
}
} ( )
2022-10-17 13:43:30 +00:00
2023-12-14 21:52:52 +00:00
api . AGPL . Options . ParseLicenseClaims = func ( rawJWT string ) ( email string , trial bool , err error ) {
c , err := license . ParseClaims ( rawJWT , Keys )
if err != nil {
return "" , false , err
}
return c . Subject , c . Trial , nil
}
2023-02-02 19:53:48 +00:00
api . AGPL . Options . SetUserGroups = api . setUserGroups
2023-07-24 12:34:24 +00:00
api . AGPL . Options . SetUserSiteRoles = api . setUserSiteRoles
2023-06-30 15:32:35 +00:00
api . AGPL . SiteHandler . RegionsFetcher = func ( ctx context . Context ) ( any , error ) {
// If the user can read the workspace proxy resource, return that.
// If not, always default to the regions.
2023-10-18 22:07:21 +00:00
actor , ok := agpldbauthz . ActorFromContext ( ctx )
2023-06-30 15:32:35 +00:00
if ok && api . Authorizer . Authorize ( ctx , actor , rbac . ActionRead , rbac . ResourceWorkspaceProxy ) == nil {
return api . fetchWorkspaceProxies ( ctx )
}
return api . fetchRegions ( ctx )
}
2024-01-18 06:10:36 +00:00
api . tailnetService , err = tailnet . NewClientService (
api . Logger . Named ( "tailnetclient" ) ,
& api . AGPL . TailnetCoordinator ,
api . Options . DERPMapUpdateFrequency ,
api . AGPL . DERPMap ,
)
if err != nil {
api . Logger . Fatal ( api . ctx , "failed to initialize tailnet client service" , slog . Error ( err ) )
}
2023-02-02 19:53:48 +00:00
2022-09-20 04:11:01 +00:00
oauthConfigs := & httpmw . OAuth2Configs {
Github : options . GithubOAuth2Config ,
OIDC : options . OIDCConfig ,
}
2023-03-07 19:38:11 +00:00
apiKeyMiddleware := httpmw . ExtractAPIKeyMW ( httpmw . ExtractAPIKeyConfig {
2024-03-25 19:01:42 +00:00
DB : options . Database ,
OAuth2Configs : oauthConfigs ,
RedirectToLogin : false ,
DisableSessionExpiryRefresh : options . DeploymentValues . DisableSessionExpiryRefresh . Value ( ) ,
Optional : false ,
SessionTokenFunc : nil , // Default behavior
PostAuthAdditionalHeadersFunc : options . PostAuthAdditionalHeadersFunc ,
2022-09-22 22:30:32 +00:00
} )
2024-02-21 22:30:33 +00:00
// Same as above but it redirects to the login page.
apiKeyMiddlewareRedirect := httpmw . ExtractAPIKeyMW ( httpmw . ExtractAPIKeyConfig {
2024-03-25 19:01:42 +00:00
DB : options . Database ,
OAuth2Configs : oauthConfigs ,
RedirectToLogin : true ,
DisableSessionExpiryRefresh : options . DeploymentValues . DisableSessionExpiryRefresh . Value ( ) ,
Optional : false ,
SessionTokenFunc : nil , // Default behavior
PostAuthAdditionalHeadersFunc : options . PostAuthAdditionalHeadersFunc ,
2024-02-21 22:30:33 +00:00
} )
2023-06-30 18:41:29 +00:00
apiKeyMiddlewareOptional := httpmw . ExtractAPIKeyMW ( httpmw . ExtractAPIKeyConfig {
2024-03-25 19:01:42 +00:00
DB : options . Database ,
OAuth2Configs : oauthConfigs ,
RedirectToLogin : false ,
DisableSessionExpiryRefresh : options . DeploymentValues . DisableSessionExpiryRefresh . Value ( ) ,
Optional : true ,
SessionTokenFunc : nil , // Default behavior
PostAuthAdditionalHeadersFunc : options . PostAuthAdditionalHeadersFunc ,
2023-06-30 18:41:29 +00:00
} )
2022-09-22 22:30:32 +00:00
2023-06-08 15:30:15 +00:00
deploymentID , err := options . Database . GetDeploymentID ( ctx )
if err != nil {
return nil , xerrors . Errorf ( "failed to get deployment ID: %w" , err )
}
2024-02-20 23:58:43 +00:00
api . AGPL . RootHandler . Group ( func ( r chi . Router ) {
2024-02-21 22:30:33 +00:00
// OAuth2 linking routes do not make sense under the /api/v2 path.
2024-02-21 02:01:25 +00:00
r . Route ( "/oauth2" , func ( r chi . Router ) {
r . Use (
api . oAuth2ProviderMiddleware ,
// Fetch the app as system because in the /tokens route there will be no
// authenticated user.
httpmw . AsAuthzSystem ( httpmw . ExtractOAuth2ProviderApp ( options . Database ) ) ,
)
2024-02-21 22:30:33 +00:00
r . Route ( "/authorize" , func ( r chi . Router ) {
r . Use ( apiKeyMiddlewareRedirect )
r . Get ( "/" , api . getOAuth2ProviderAppAuthorize ( ) )
} )
r . Route ( "/tokens" , func ( r chi . Router ) {
r . Group ( func ( r chi . Router ) {
r . Use ( apiKeyMiddleware )
// DELETE on /tokens is not part of the OAuth2 spec. It is our own
// route used to revoke permissions from an application. It is here for
// parity with POST on /tokens.
r . Delete ( "/" , api . deleteOAuth2ProviderAppTokens ( ) )
} )
// The POST /tokens endpoint will be called from an unauthorized client so we
// cannot require an API key.
r . Post ( "/" , api . postOAuth2ProviderAppToken ( ) )
2024-02-20 23:58:43 +00:00
} )
} )
} )
2024-02-23 16:48:24 +00:00
api . AGPL . RefreshEntitlements = func ( ctx context . Context ) error {
return api . refreshEntitlements ( ctx )
}
2024-02-20 23:58:43 +00:00
2022-09-20 04:11:01 +00:00
api . AGPL . APIHandler . Group ( func ( r chi . Router ) {
r . Get ( "/entitlements" , api . serveEntitlements )
2023-04-25 14:37:52 +00:00
// /regions overrides the AGPL /regions endpoint
r . Group ( func ( r chi . Router ) {
r . Use ( apiKeyMiddleware )
r . Get ( "/regions" , api . regions )
} )
2022-10-17 13:43:30 +00:00
r . Route ( "/replicas" , func ( r chi . Router ) {
r . Use ( apiKeyMiddleware )
r . Get ( "/" , api . replicas )
} )
2022-09-20 04:11:01 +00:00
r . Route ( "/licenses" , func ( r chi . Router ) {
r . Use ( apiKeyMiddleware )
2023-08-22 14:26:43 +00:00
r . Post ( "/refresh-entitlements" , api . postRefreshEntitlements )
2022-09-20 04:11:01 +00:00
r . Post ( "/" , api . postLicense )
r . Get ( "/" , api . licenses )
r . Delete ( "/{id}" , api . deleteLicense )
} )
2023-04-20 23:59:45 +00:00
r . Route ( "/applications/reconnecting-pty-signed-token" , func ( r chi . Router ) {
r . Use ( apiKeyMiddleware )
r . Post ( "/" , api . reconnectingPTYSignedToken )
} )
2023-04-04 20:07:29 +00:00
r . Route ( "/workspaceproxies" , func ( r chi . Router ) {
r . Use (
api . moonsEnabledMW ,
)
2023-04-17 19:57:21 +00:00
r . Group ( func ( r chi . Router ) {
r . Use (
apiKeyMiddleware ,
)
r . Post ( "/" , api . postWorkspaceProxy )
r . Get ( "/" , api . workspaceProxies )
} )
r . Route ( "/me" , func ( r chi . Router ) {
r . Use (
httpmw . ExtractWorkspaceProxy ( httpmw . ExtractWorkspaceProxyConfig {
DB : options . Database ,
Optional : false ,
} ) ,
)
2023-07-19 16:11:11 +00:00
r . Get ( "/coordinate" , api . workspaceProxyCoordinate )
2023-04-17 19:57:21 +00:00
r . Post ( "/issue-signed-app-token" , api . workspaceProxyIssueSignedAppToken )
2023-08-16 12:22:00 +00:00
r . Post ( "/app-stats" , api . workspaceProxyReportAppStats )
2023-04-20 14:48:47 +00:00
r . Post ( "/register" , api . workspaceProxyRegister )
2023-07-26 16:21:04 +00:00
r . Post ( "/deregister" , api . workspaceProxyDeregister )
2023-04-20 14:48:47 +00:00
} )
r . Route ( "/{workspaceproxy}" , func ( r chi . Router ) {
r . Use (
apiKeyMiddleware ,
2023-06-08 15:30:15 +00:00
httpmw . ExtractWorkspaceProxyParam ( api . Database , deploymentID , api . AGPL . PrimaryWorkspaceProxy ) ,
2023-04-20 14:48:47 +00:00
)
2023-05-09 18:46:50 +00:00
r . Get ( "/" , api . workspaceProxy )
r . Patch ( "/" , api . patchWorkspaceProxy )
2023-04-20 14:48:47 +00:00
r . Delete ( "/" , api . deleteWorkspaceProxy )
2023-04-17 19:57:21 +00:00
} )
2023-04-04 20:07:29 +00:00
} )
2022-10-10 20:37:06 +00:00
r . Route ( "/organizations/{organization}/groups" , func ( r chi . Router ) {
r . Use (
apiKeyMiddleware ,
2022-10-27 21:49:35 +00:00
api . templateRBACEnabledMW ,
2022-10-10 20:37:06 +00:00
httpmw . ExtractOrganizationParam ( api . Database ) ,
)
r . Post ( "/" , api . postGroupByOrganization )
2023-01-11 15:05:42 +00:00
r . Get ( "/" , api . groupsByOrganization )
2022-10-27 21:49:35 +00:00
r . Route ( "/{groupName}" , func ( r chi . Router ) {
r . Use (
httpmw . ExtractGroupByNameParam ( api . Database ) ,
)
2023-01-13 11:27:21 +00:00
r . Get ( "/" , api . groupByOrganization )
2022-10-27 21:49:35 +00:00
} )
2022-10-10 20:37:06 +00:00
} )
2023-08-04 08:32:28 +00:00
// TODO: provisioner daemons are not scoped to organizations in the database, so placing them
// under an organization route doesn't make sense. In order to allow the /serve endpoint to
// work with a pre-shared key (PSK) without an API key, these routes will simply ignore the
// value of {organization}. That is, the route will work with any organization ID, whether or
// not it exits. This doesn't leak any information about the existence of organizations, so is
// fine from a security perspective, but might be a little surprising.
//
// We may in future decide to scope provisioner daemons to organizations, so we'll keep the API
// route as is.
2022-11-16 22:34:06 +00:00
r . Route ( "/organizations/{organization}/provisionerdaemons" , func ( r chi . Router ) {
r . Use (
api . provisionerDaemonsEnabledMW ,
2024-03-04 21:15:41 +00:00
apiKeyMiddlewareOptional ,
httpmw . ExtractProvisionerDaemonAuthenticated ( httpmw . ExtractProvisionerAuthConfig {
DB : api . Database ,
Optional : true ,
} , api . ProvisionerDaemonPSK ) ,
// Either a user auth or provisioner auth is required
// to move forward.
httpmw . RequireAPIKeyOrProvisionerDaemonAuth ( ) ,
httpmw . ExtractOrganizationParam ( api . Database ) ,
2022-11-16 22:34:06 +00:00
)
2023-08-04 08:32:28 +00:00
r . With ( apiKeyMiddleware ) . Get ( "/" , api . provisionerDaemons )
r . With ( apiKeyMiddlewareOptional ) . Get ( "/serve" , api . provisionerDaemonServe )
2022-11-16 22:34:06 +00:00
} )
2022-10-10 20:37:06 +00:00
r . Route ( "/templates/{template}/acl" , func ( r chi . Router ) {
r . Use (
2022-10-11 18:51:41 +00:00
api . templateRBACEnabledMW ,
2022-10-10 20:37:06 +00:00
apiKeyMiddleware ,
httpmw . ExtractTemplateParam ( api . Database ) ,
)
2023-07-26 14:33:48 +00:00
r . Get ( "/available" , api . templateAvailablePermissions )
2022-10-10 20:37:06 +00:00
r . Get ( "/" , api . templateACL )
r . Patch ( "/" , api . patchTemplateACL )
} )
r . Route ( "/groups/{group}" , func ( r chi . Router ) {
r . Use (
2022-10-11 18:51:41 +00:00
api . templateRBACEnabledMW ,
2022-10-10 20:37:06 +00:00
apiKeyMiddleware ,
httpmw . ExtractGroupParam ( api . Database ) ,
)
r . Get ( "/" , api . group )
r . Patch ( "/" , api . patchGroup )
r . Delete ( "/" , api . deleteGroup )
} )
2022-09-30 18:01:20 +00:00
r . Route ( "/workspace-quota" , func ( r chi . Router ) {
2022-11-14 17:57:33 +00:00
r . Use (
apiKeyMiddleware ,
)
2022-09-30 18:01:20 +00:00
r . Route ( "/{user}" , func ( r chi . Router ) {
2023-10-10 12:13:00 +00:00
r . Use ( httpmw . ExtractUserParam ( options . Database ) )
2022-09-30 18:01:20 +00:00
r . Get ( "/" , api . workspaceQuota )
} )
} )
2023-01-04 21:31:45 +00:00
r . Route ( "/appearance" , func ( r chi . Router ) {
2023-06-30 18:41:29 +00:00
r . Group ( func ( r chi . Router ) {
r . Use (
apiKeyMiddlewareOptional ,
2024-03-14 16:27:32 +00:00
httpmw . ExtractWorkspaceAgentAndLatestBuild ( httpmw . ExtractWorkspaceAgentAndLatestBuildConfig {
2023-06-30 18:41:29 +00:00
DB : options . Database ,
Optional : true ,
} ) ,
httpmw . RequireAPIKeyOrWorkspaceAgent ( ) ,
)
r . Get ( "/" , api . appearance )
} )
r . Group ( func ( r chi . Router ) {
r . Use (
apiKeyMiddleware ,
)
r . Put ( "/" , api . putAppearance )
} )
2022-12-06 18:38:38 +00:00
} )
2023-07-20 13:35:41 +00:00
r . Route ( "/users/{user}/quiet-hours" , func ( r chi . Router ) {
r . Use (
2023-08-29 18:35:05 +00:00
api . autostopRequirementEnabledMW ,
2023-07-20 13:35:41 +00:00
apiKeyMiddleware ,
2023-10-10 12:13:00 +00:00
httpmw . ExtractUserParam ( options . Database ) ,
2023-07-20 13:35:41 +00:00
)
r . Get ( "/" , api . userQuietHoursSchedule )
r . Put ( "/" , api . putUserQuietHoursSchedule )
} )
2023-12-21 21:38:42 +00:00
r . Route ( "/oauth2-provider" , func ( r chi . Router ) {
r . Use (
apiKeyMiddleware ,
api . oAuth2ProviderMiddleware ,
)
r . Route ( "/apps" , func ( r chi . Router ) {
r . Get ( "/" , api . oAuth2ProviderApps )
r . Post ( "/" , api . postOAuth2ProviderApp )
r . Route ( "/{app}" , func ( r chi . Router ) {
r . Use ( httpmw . ExtractOAuth2ProviderApp ( options . Database ) )
r . Get ( "/" , api . oAuth2ProviderApp )
r . Put ( "/" , api . putOAuth2ProviderApp )
r . Delete ( "/" , api . deleteOAuth2ProviderApp )
r . Route ( "/secrets" , func ( r chi . Router ) {
r . Get ( "/" , api . oAuth2ProviderAppSecrets )
r . Post ( "/" , api . postOAuth2ProviderAppSecret )
r . Route ( "/{secretID}" , func ( r chi . Router ) {
r . Use ( httpmw . ExtractOAuth2ProviderAppSecret ( options . Database ) )
r . Delete ( "/" , api . deleteOAuth2ProviderAppSecret )
} )
} )
} )
} )
} )
2024-01-30 01:30:02 +00:00
r . Route ( "/integrations" , func ( r chi . Router ) {
r . Use (
apiKeyMiddleware ,
api . jfrogEnabledMW ,
)
r . Post ( "/jfrog/xray-scan" , api . postJFrogXrayScan )
r . Get ( "/jfrog/xray-scan" , api . jFrogXrayScan )
} )
2022-09-20 04:11:01 +00:00
} )
2022-09-20 20:16:26 +00:00
if len ( options . SCIMAPIKey ) != 0 {
api . AGPL . RootHandler . Route ( "/scim/v2" , func ( r chi . Router ) {
2023-02-14 14:27:06 +00:00
r . Use (
api . scimEnabledMW ,
)
2022-09-20 20:16:26 +00:00
r . Post ( "/Users" , api . scimPostUser )
r . Route ( "/Users" , func ( r chi . Router ) {
r . Get ( "/" , api . scimGetUsers )
r . Post ( "/" , api . scimPostUser )
r . Get ( "/{id}" , api . scimGetUser )
r . Patch ( "/{id}" , api . scimPatchUser )
} )
} )
}
2024-03-08 06:31:40 +00:00
meshTLSConfig , err := replicasync . CreateDERPMeshTLSConfig ( options . AccessURL . Hostname ( ) , options . TLSCertificates )
if err != nil {
return nil , xerrors . Errorf ( "create DERP mesh TLS config: %w" , err )
2022-10-17 13:43:30 +00:00
}
2024-03-21 21:53:41 +00:00
// We always want to run the replica manager even if we don't have DERP
// enabled, since it's used to detect other coder servers for licensing.
2022-10-17 13:43:30 +00:00
api . replicaManager , err = replicasync . New ( ctx , options . Logger , options . Database , options . Pubsub , & replicasync . Options {
2023-07-26 16:21:04 +00:00
ID : api . AGPL . ID ,
RelayAddress : options . DERPServerRelayAddress ,
RegionID : int32 ( options . DERPServerRegionID ) ,
TLSConfig : meshTLSConfig ,
UpdateInterval : options . ReplicaSyncUpdateInterval ,
2022-10-17 13:43:30 +00:00
} )
if err != nil {
return nil , xerrors . Errorf ( "initialize replica: %w" , err )
}
2024-03-21 21:53:41 +00:00
if api . DERPServer != nil {
api . derpMesh = derpmesh . New ( options . Logger . Named ( "derpmesh" ) , api . DERPServer , meshTLSConfig )
}
2022-10-17 13:43:30 +00:00
2023-12-19 20:40:22 +00:00
// Moon feature init. Proxyhealh is a go routine to periodically check
// the health of all workspace proxies.
api . ProxyHealth , err = proxyhealth . New ( & proxyhealth . Options {
Interval : options . ProxyHealthInterval ,
DB : api . Database ,
Logger : options . Logger . Named ( "proxyhealth" ) ,
Client : api . HTTPClient ,
Prometheus : api . PrometheusRegistry ,
} )
if err != nil {
return nil , xerrors . Errorf ( "initialize proxy health: %w" , err )
}
go api . ProxyHealth . Run ( ctx )
// Force the initial loading of the cache. Do this in a go routine in case
// the calls to the workspace proxies hang and this takes some time.
go api . forceWorkspaceProxyHealthUpdate ( ctx )
2023-05-02 13:30:44 +00:00
2023-12-19 20:40:22 +00:00
// Use proxy health to return the healthy workspace proxy hostnames.
f := api . ProxyHealth . ProxyHosts
api . AGPL . WorkspaceProxyHostsFn . Store ( & f )
2023-11-24 15:06:51 +00:00
2023-12-19 20:40:22 +00:00
// Wire this up to healthcheck.
var fetchUpdater healthcheck . WorkspaceProxiesFetchUpdater = & workspaceProxiesFetchUpdater {
fetchFunc : api . fetchWorkspaceProxies ,
updateFunc : api . ProxyHealth . ForceUpdate ,
2023-04-24 15:25:35 +00:00
}
2023-12-19 20:40:22 +00:00
api . AGPL . WorkspaceProxiesFetchUpdater . Store ( & fetchUpdater )
2023-04-24 15:25:35 +00:00
2023-10-13 08:10:16 +00:00
err = api . PrometheusRegistry . Register ( & api . licenseMetricsCollector )
if err != nil {
return nil , xerrors . Errorf ( "unable to register license metrics collector" )
}
2022-10-17 13:43:30 +00:00
err = api . updateEntitlements ( ctx )
2022-09-20 04:11:01 +00:00
if err != nil {
return nil , xerrors . Errorf ( "update entitlements: %w" , err )
}
go api . runEntitlementsLoop ( ctx )
return api , nil
}
type Options struct {
* coderd . Options
2022-10-17 13:43:30 +00:00
RBAC bool
2022-09-22 15:14:22 +00:00
AuditLogging bool
// Whether to block non-browser connections.
2022-11-14 17:57:33 +00:00
BrowserOnly bool
SCIMAPIKey [ ] byte
2022-09-30 18:01:20 +00:00
2023-09-07 14:49:49 +00:00
ExternalTokenEncryption [ ] dbcrypt . Cipher
2022-10-17 13:43:30 +00:00
// Used for high availability.
2023-07-26 16:21:04 +00:00
ReplicaSyncUpdateInterval time . Duration
DERPServerRelayAddress string
DERPServerRegionID int
2022-10-17 13:43:30 +00:00
2023-07-20 13:35:41 +00:00
// Used for user quiet hours schedules.
DefaultQuietHoursSchedule string // cron schedule, if empty user quiet hours schedules are disabled
2022-09-20 04:11:01 +00:00
EntitlementsUpdateInterval time . Duration
2023-04-24 15:25:35 +00:00
ProxyHealthInterval time . Duration
2023-09-07 14:49:49 +00:00
LicenseKeys map [ string ] ed25519 . PublicKey
2023-08-04 08:32:28 +00:00
// optional pre-shared key for authentication of external provisioner daemons
ProvisionerDaemonPSK string
2023-08-31 15:59:53 +00:00
CheckInactiveUsersCancelFunc func ( )
2022-09-20 04:11:01 +00:00
}
type API struct {
AGPL * coderd . API
* Options
2023-04-24 15:25:35 +00:00
// ctx is canceled immediately on shutdown, it can be used to abort
// interruptible tasks.
ctx context . Context
cancel context . CancelFunc
2022-10-17 13:43:30 +00:00
// Detects multiple Coder replicas running at the same time.
replicaManager * replicasync . Manager
// Meshes DERP connections from multiple replicas.
derpMesh * derpmesh . Mesh
2023-04-25 14:37:52 +00:00
// ProxyHealth checks the reachability of all workspace proxies.
ProxyHealth * proxyhealth . ProxyHealth
2022-10-17 13:43:30 +00:00
2023-06-26 17:22:28 +00:00
entitlementsUpdateMu sync . Mutex
entitlementsMu sync . RWMutex
entitlements codersdk . Entitlements
2023-08-04 08:32:28 +00:00
provisionerDaemonAuth * provisionerDaemonAuth
2023-10-13 08:10:16 +00:00
licenseMetricsCollector license . MetricsCollector
2024-01-18 06:10:36 +00:00
tailnetService * tailnet . ClientService
2022-09-20 04:11:01 +00:00
}
2024-03-25 19:01:42 +00:00
// writeEntitlementWarningsHeader writes the entitlement warnings to the response header
// for all authenticated users with roles. If there are no warnings, this header will not be written.
//
// This header is used by the CLI to display warnings to the user without having
// to make additional requests!
func ( api * API ) writeEntitlementWarningsHeader ( a httpmw . Authorization , header http . Header ) {
roles , err := a . Actor . Roles . Expand ( )
if err != nil {
return
}
nonMemberRoles := 0
for _ , role := range roles {
// The member role is implied, and not assignable.
// If there is no display name, then the role is also unassigned.
// This is not the ideal logic, but works for now.
if role . Name == rbac . RoleMember ( ) || ( role . DisplayName == "" ) {
continue
}
nonMemberRoles ++
}
if nonMemberRoles == 0 {
// Don't show entitlement warnings if the user
// has no roles. This is a normal user!
return
}
api . entitlementsMu . RLock ( )
defer api . entitlementsMu . RUnlock ( )
for _ , warning := range api . entitlements . Warnings {
header . Add ( codersdk . EntitlementsWarningHeader , warning )
}
}
2022-09-20 04:11:01 +00:00
func ( api * API ) Close ( ) error {
2023-08-22 14:26:43 +00:00
// Replica manager should be closed first. This is because the replica
// manager updates the replica's table in the database when it closes.
// This tells other Coderds that it is now offline.
2023-06-13 17:18:31 +00:00
if api . replicaManager != nil {
_ = api . replicaManager . Close ( )
}
2023-08-22 14:26:43 +00:00
api . cancel ( )
2023-06-13 17:18:31 +00:00
if api . derpMesh != nil {
_ = api . derpMesh . Close ( )
}
2023-08-31 15:59:53 +00:00
if api . Options . CheckInactiveUsersCancelFunc != nil {
api . Options . CheckInactiveUsersCancelFunc ( )
}
2022-09-20 04:11:01 +00:00
return api . AGPL . Close ( )
}
func ( api * API ) updateEntitlements ( ctx context . Context ) error {
2023-06-26 17:22:28 +00:00
api . entitlementsUpdateMu . Lock ( )
defer api . entitlementsUpdateMu . Unlock ( )
2022-09-20 04:11:01 +00:00
2023-02-15 01:40:08 +00:00
entitlements , err := license . Entitlements (
ctx , api . Database ,
2023-09-29 19:13:20 +00:00
api . Logger , len ( api . replicaManager . AllPrimary ( ) ) , len ( api . ExternalAuthConfigs ) , api . LicenseKeys , map [ codersdk . FeatureName ] bool {
2023-02-15 01:40:08 +00:00
codersdk . FeatureAuditLog : api . AuditLogging ,
codersdk . FeatureBrowserOnly : api . BrowserOnly ,
codersdk . FeatureSCIM : len ( api . SCIMAPIKey ) != 0 ,
2023-10-03 15:27:02 +00:00
codersdk . FeatureMultipleExternalAuth : len ( api . ExternalAuthConfigs ) > 1 ,
2023-12-21 21:38:42 +00:00
codersdk . FeatureOAuth2Provider : true ,
2023-02-15 01:40:08 +00:00
codersdk . FeatureTemplateRBAC : api . RBAC ,
2023-09-07 14:49:49 +00:00
codersdk . FeatureExternalTokenEncryption : len ( api . ExternalTokenEncryption ) > 0 ,
2023-02-15 01:40:08 +00:00
codersdk . FeatureExternalProvisionerDaemons : true ,
2023-03-07 14:14:58 +00:00
codersdk . FeatureAdvancedTemplateScheduling : true ,
2023-12-15 08:27:56 +00:00
codersdk . FeatureWorkspaceProxy : true ,
codersdk . FeatureUserRoleManagement : true ,
codersdk . FeatureAccessControl : true ,
2024-02-13 14:31:20 +00:00
codersdk . FeatureControlSharedPorts : true ,
2023-02-15 01:40:08 +00:00
} )
2022-10-07 00:28:22 +00:00
if err != nil {
return err
2022-09-20 04:11:01 +00:00
}
2023-02-14 20:26:47 +00:00
2023-03-07 21:10:01 +00:00
if entitlements . RequireTelemetry && ! api . DeploymentValues . Telemetry . Enable . Value ( ) {
2023-02-14 20:26:47 +00:00
// We can't fail because then the user couldn't remove the offending
// license w/o a restart.
2023-02-15 01:40:08 +00:00
//
// We don't simply append to entitlement.Errors since we don't want any
// enterprise features enabled.
2023-02-14 20:26:47 +00:00
api . entitlements . Errors = [ ] string {
"License requires telemetry but telemetry is disabled" ,
}
api . Logger . Error ( ctx , "license requires telemetry enabled" )
return nil
}
2023-07-19 15:32:29 +00:00
featureChanged := func ( featureName codersdk . FeatureName ) ( initial , changed , enabled bool ) {
2022-10-07 00:28:22 +00:00
if api . entitlements . Features == nil {
2023-07-19 15:32:29 +00:00
return true , false , entitlements . Features [ featureName ] . Enabled
2022-09-20 04:11:01 +00:00
}
2022-10-07 00:28:22 +00:00
oldFeature := api . entitlements . Features [ featureName ]
newFeature := entitlements . Features [ featureName ]
if oldFeature . Enabled != newFeature . Enabled {
2023-07-19 15:32:29 +00:00
return false , true , newFeature . Enabled
2022-09-30 18:01:20 +00:00
}
2023-07-19 15:32:29 +00:00
return false , false , newFeature . Enabled
2022-09-20 04:11:01 +00:00
}
2023-07-19 15:32:29 +00:00
shouldUpdate := func ( initial , changed , enabled bool ) bool {
// Avoid an initial tick on startup unless the feature is enabled.
return changed || ( initial && enabled )
}
if initial , changed , enabled := featureChanged ( codersdk . FeatureAuditLog ) ; shouldUpdate ( initial , changed , enabled ) {
2022-10-07 00:28:22 +00:00
auditor := agplaudit . NewNop ( )
if enabled {
2022-10-19 07:00:45 +00:00
auditor = api . AGPL . Options . Auditor
2022-09-20 04:11:01 +00:00
}
2022-10-07 00:28:22 +00:00
api . AGPL . Auditor . Store ( & auditor )
2022-09-20 04:11:01 +00:00
}
2023-07-19 15:32:29 +00:00
if initial , changed , enabled := featureChanged ( codersdk . FeatureBrowserOnly ) ; shouldUpdate ( initial , changed , enabled ) {
2022-09-22 15:14:22 +00:00
var handler func ( rw http . ResponseWriter ) bool
2022-10-07 00:28:22 +00:00
if enabled {
2022-09-22 15:14:22 +00:00
handler = api . shouldBlockNonBrowserConnections
}
api . AGPL . WorkspaceClientCoordinateOverride . Store ( & handler )
}
2023-07-19 15:32:29 +00:00
if initial , changed , enabled := featureChanged ( codersdk . FeatureTemplateRBAC ) ; shouldUpdate ( initial , changed , enabled ) {
2022-10-07 00:28:22 +00:00
if enabled {
2023-08-22 02:55:39 +00:00
committer := committer {
Log : api . Logger . Named ( "quota_committer" ) ,
Database : api . Database ,
}
2023-11-24 15:06:51 +00:00
qcPtr := proto . QuotaCommitter ( & committer )
api . AGPL . QuotaCommitter . Store ( & qcPtr )
2022-11-14 17:57:33 +00:00
} else {
api . AGPL . QuotaCommitter . Store ( nil )
2022-09-30 18:01:20 +00:00
}
}
2023-07-19 15:32:29 +00:00
if initial , changed , enabled := featureChanged ( codersdk . FeatureAdvancedTemplateScheduling ) ; shouldUpdate ( initial , changed , enabled ) {
2023-03-07 14:14:58 +00:00
if enabled {
2023-08-14 21:16:47 +00:00
templateStore := schedule . NewEnterpriseTemplateScheduleStore ( api . AGPL . UserQuietHoursScheduleStore )
2023-07-20 13:35:41 +00:00
templateStoreInterface := agplschedule . TemplateScheduleStore ( templateStore )
api . AGPL . TemplateScheduleStore . Store ( & templateStoreInterface )
2023-12-15 08:27:56 +00:00
if api . DefaultQuietHoursSchedule == "" {
api . Logger . Warn ( ctx , "template autostop requirement will default to UTC midnight as the default user quiet hours schedule. Set a custom default quiet hours schedule using CODER_QUIET_HOURS_DEFAULT_SCHEDULE to avoid this warning" )
api . DefaultQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *"
2023-07-20 13:35:41 +00:00
}
2023-12-15 09:33:51 +00:00
quietHoursStore , err := schedule . NewEnterpriseUserQuietHoursScheduleStore ( api . DefaultQuietHoursSchedule , api . DeploymentValues . UserQuietHoursSchedule . AllowUserCustom . Value ( ) )
2023-07-20 13:35:41 +00:00
if err != nil {
2023-08-29 18:35:05 +00:00
api . Logger . Error ( ctx , "unable to set up enterprise user quiet hours schedule store, template autostop requirements will not be applied to workspace builds" , slog . Error ( err ) )
2023-07-20 13:35:41 +00:00
} else {
api . AGPL . UserQuietHoursScheduleStore . Store ( & quietHoursStore )
}
2023-03-07 14:14:58 +00:00
} else {
2023-12-15 08:27:56 +00:00
templateStore := agplschedule . NewAGPLTemplateScheduleStore ( )
api . AGPL . TemplateScheduleStore . Store ( & templateStore )
2023-07-20 13:35:41 +00:00
quietHoursStore := agplschedule . NewAGPLUserQuietHoursScheduleStore ( )
api . AGPL . UserQuietHoursScheduleStore . Store ( & quietHoursStore )
2023-03-07 14:14:58 +00:00
}
}
2023-07-19 15:32:29 +00:00
if initial , changed , enabled := featureChanged ( codersdk . FeatureHighAvailability ) ; shouldUpdate ( initial , changed , enabled ) {
var coordinator agpltailnet . Coordinator
2022-10-25 14:33:37 +00:00
if enabled {
2024-01-05 04:03:36 +00:00
haCoordinator , err := tailnet . NewPGCoord ( api . ctx , api . Logger , api . Pubsub , api . Database )
2022-10-17 13:43:30 +00:00
if err != nil {
api . Logger . Error ( ctx , "unable to set up high availability coordinator" , slog . Error ( err ) )
// If we try to setup the HA coordinator and it fails, nothing
// is actually changing.
} else {
coordinator = haCoordinator
}
api . replicaManager . SetCallback ( func ( ) {
2024-03-21 21:53:41 +00:00
// Only update DERP mesh if the built-in server is enabled.
if api . Options . DeploymentValues . DERP . Server . Enable {
addresses := make ( [ ] string , 0 )
for _ , replica := range api . replicaManager . Regional ( ) {
// Don't add replicas with an empty relay address.
if replica . RelayAddress == "" {
continue
}
addresses = append ( addresses , replica . RelayAddress )
}
api . derpMesh . SetAddresses ( addresses , false )
2022-10-17 13:43:30 +00:00
}
_ = api . updateEntitlements ( ctx )
} )
} else {
2023-07-19 15:32:29 +00:00
coordinator = agpltailnet . NewCoordinator ( api . Logger )
2022-10-17 13:43:30 +00:00
api . derpMesh . SetAddresses ( [ ] string { } , false )
api . replicaManager . SetCallback ( func ( ) {
// If the amount of replicas change, so should our entitlements.
// This is to display a warning in the UI if the user is unlicensed.
_ = api . updateEntitlements ( ctx )
} )
}
// Recheck changed in case the HA coordinator failed to set up.
2023-07-19 15:32:29 +00:00
if coordinator != nil {
2022-10-17 13:43:30 +00:00
oldCoordinator := * api . AGPL . TailnetCoordinator . Swap ( & coordinator )
err := oldCoordinator . Close ( )
if err != nil {
api . Logger . Error ( ctx , "close old tailnet coordinator" , slog . Error ( err ) )
}
}
}
2023-07-26 16:21:04 +00:00
if initial , changed , enabled := featureChanged ( codersdk . FeatureWorkspaceProxy ) ; shouldUpdate ( initial , changed , enabled ) {
if enabled {
2023-08-10 19:04:17 +00:00
fn := derpMapper ( api . Logger , api . ProxyHealth )
2023-07-26 16:21:04 +00:00
api . AGPL . DERPMapper . Store ( & fn )
} else {
api . AGPL . DERPMapper . Store ( nil )
}
}
2023-10-18 22:07:21 +00:00
if initial , changed , enabled := featureChanged ( codersdk . FeatureAccessControl ) ; shouldUpdate ( initial , changed , enabled ) {
var acs agpldbauthz . AccessControlStore = agpldbauthz . AGPLTemplateAccessControlStore { }
if enabled {
acs = dbauthz . EnterpriseTemplateAccessControlStore { }
}
api . AGPL . AccessControlStore . Store ( & acs )
}
2024-01-29 08:17:31 +00:00
if initial , changed , enabled := featureChanged ( codersdk . FeatureAppearance ) ; shouldUpdate ( initial , changed , enabled ) {
if enabled {
f := newAppearanceFetcher (
api . Database ,
api . DeploymentValues . Support . Links . Value ,
)
api . AGPL . AppearanceFetcher . Store ( & f )
} else {
api . AGPL . AppearanceFetcher . Store ( & appearance . DefaultFetcher )
}
}
2024-02-13 14:31:20 +00:00
if initial , changed , enabled := featureChanged ( codersdk . FeatureControlSharedPorts ) ; shouldUpdate ( initial , changed , enabled ) {
var ps agplportsharing . PortSharer = agplportsharing . DefaultPortSharer
if enabled {
ps = portsharing . NewEnterprisePortSharer ( )
}
api . AGPL . PortSharer . Store ( & ps )
}
2023-09-07 14:49:49 +00:00
// External token encryption is soft-enforced
featureExternalTokenEncryption := entitlements . Features [ codersdk . FeatureExternalTokenEncryption ]
featureExternalTokenEncryption . Enabled = len ( api . ExternalTokenEncryption ) > 0
if featureExternalTokenEncryption . Enabled && featureExternalTokenEncryption . Entitlement != codersdk . EntitlementEntitled {
msg := fmt . Sprintf ( "%s is enabled (due to setting external token encryption keys) but your license is not entitled to this feature." , codersdk . FeatureExternalTokenEncryption . Humanize ( ) )
api . Logger . Warn ( ctx , msg )
entitlements . Warnings = append ( entitlements . Warnings , msg )
}
entitlements . Features [ codersdk . FeatureExternalTokenEncryption ] = featureExternalTokenEncryption
2023-06-26 17:22:28 +00:00
api . entitlementsMu . Lock ( )
defer api . entitlementsMu . Unlock ( )
2022-09-20 04:11:01 +00:00
api . entitlements = entitlements
2023-10-13 08:10:16 +00:00
api . licenseMetricsCollector . Entitlements . Store ( & entitlements )
2023-06-18 18:57:27 +00:00
api . AGPL . SiteHandler . Entitlements . Store ( & entitlements )
2022-09-20 04:11:01 +00:00
return nil
}
2023-07-26 16:21:04 +00:00
// getProxyDERPStartingRegionID returns the starting region ID that should be
// used for workspace proxies. A proxy's actual region ID is the return value
// from this function + it's RegionID field.
//
// Two ints are returned, the first is the starting region ID for proxies, and
// the second is the maximum region ID that already exists in the DERP map.
func getProxyDERPStartingRegionID ( derpMap * tailcfg . DERPMap ) ( sID int64 , mID int64 ) {
var maxRegionID int64
for _ , region := range derpMap . Regions {
rid := int64 ( region . RegionID )
if rid > maxRegionID {
maxRegionID = rid
}
}
if maxRegionID < 0 {
maxRegionID = 0
}
// Round to the nearest 10,000 with a sufficient buffer of at least 2,000.
// The buffer allows for future "fixed" regions to be added to the base DERP
// map without conflicting with proxy region IDs (standard DERP maps usually
// use incrementing IDs for new regions).
//
// Example:
// maxRegionID = -2_000 -> startingRegionID = 10_000
// maxRegionID = 8_000 -> startingRegionID = 10_000
// maxRegionID = 8_500 -> startingRegionID = 20_000
// maxRegionID = 12_000 -> startingRegionID = 20_000
// maxRegionID = 20_000 -> startingRegionID = 30_000
const roundStartingRegionID = 10_000
const startingRegionIDBuffer = 2_000
// Add the buffer first.
startingRegionID := maxRegionID + startingRegionIDBuffer
// Round UP to the nearest 10,000. Go's math.Ceil rounds up to the nearest
// integer, so we need to divide by 10,000 first and then multiply by
// 10,000.
startingRegionID = int64 ( math . Ceil ( float64 ( startingRegionID ) / roundStartingRegionID ) * roundStartingRegionID )
// This should never be hit but it's here just in case.
if startingRegionID < roundStartingRegionID {
startingRegionID = roundStartingRegionID
}
return startingRegionID , maxRegionID
}
var (
lastDerpConflictMutex sync . Mutex
lastDerpConflictLog time . Time
)
2023-08-10 19:04:17 +00:00
func derpMapper ( logger slog . Logger , proxyHealth * proxyhealth . ProxyHealth ) func ( * tailcfg . DERPMap ) * tailcfg . DERPMap {
2023-07-26 16:21:04 +00:00
return func ( derpMap * tailcfg . DERPMap ) * tailcfg . DERPMap {
derpMap = derpMap . Clone ( )
// Find the starting region ID that we'll use for proxies. This must be
// deterministic based on the derp map.
startingRegionID , largestRegionID := getProxyDERPStartingRegionID ( derpMap )
if largestRegionID >= 1 << 32 {
// Enforce an upper bound on the region ID. This shouldn't be hit in
// practice, but it's a good sanity check.
lastDerpConflictMutex . Lock ( )
shouldLog := lastDerpConflictLog . IsZero ( ) || time . Since ( lastDerpConflictLog ) > time . Minute
if shouldLog {
lastDerpConflictLog = time . Now ( )
}
lastDerpConflictMutex . Unlock ( )
if shouldLog {
logger . Warn (
context . Background ( ) ,
"existing DERP region IDs are too large, proxy region IDs will not be populated in the derp map. Please ensure that all DERP region IDs are less than 2^32" ,
slog . F ( "largest_region_id" , largestRegionID ) ,
2023-07-26 17:30:47 +00:00
slog . F ( "max_region_id" , int64 ( 1 << 32 - 1 ) ) ,
2023-07-26 16:21:04 +00:00
)
return derpMap
}
}
// Add all healthy proxies to the DERP map.
statusMap := proxyHealth . HealthStatus ( )
statusLoop :
for _ , status := range statusMap {
if status . Status != proxyhealth . Healthy || ! status . Proxy . DerpEnabled {
// Only add healthy proxies with DERP enabled to the DERP map.
continue
}
u , err := url . Parse ( status . Proxy . Url )
if err != nil {
// Not really any need to log, the proxy should be unreachable
// anyways and filtered out by the above condition.
continue
}
port := u . Port ( )
if port == "" {
port = "80"
if u . Scheme == "https" {
port = "443"
}
}
portInt , err := strconv . Atoi ( port )
if err != nil {
// Not really any need to log, the proxy should be unreachable
// anyways and filtered out by the above condition.
continue
}
// Sanity check that the region ID and code is unique.
//
// This should be impossible to hit as the IDs are enforced to be
// unique by the database and the computed ID is greater than any
// existing ID in the DERP map.
regionID := int ( startingRegionID ) + int ( status . Proxy . RegionID )
regionCode := fmt . Sprintf ( "coder_%s" , strings . ToLower ( status . Proxy . Name ) )
2023-08-02 03:18:46 +00:00
regionName := status . Proxy . DisplayName
if regionName == "" {
regionName = status . Proxy . Name
}
2023-07-26 16:21:04 +00:00
for _ , r := range derpMap . Regions {
if r . RegionID == regionID || r . RegionCode == regionCode {
// Log a warning if we haven't logged one in the last
// minute.
lastDerpConflictMutex . Lock ( )
shouldLog := lastDerpConflictLog . IsZero ( ) || time . Since ( lastDerpConflictLog ) > time . Minute
if shouldLog {
lastDerpConflictLog = time . Now ( )
}
lastDerpConflictMutex . Unlock ( )
if shouldLog {
logger . Warn ( context . Background ( ) ,
2023-08-01 15:50:43 +00:00
"proxy region ID or code conflict, ignoring workspace proxy for DERP map" ,
2023-07-26 16:21:04 +00:00
slog . F ( "proxy_id" , status . Proxy . ID ) ,
slog . F ( "proxy_name" , status . Proxy . Name ) ,
slog . F ( "proxy_display_name" , status . Proxy . DisplayName ) ,
slog . F ( "proxy_url" , status . Proxy . Url ) ,
slog . F ( "proxy_region_id" , status . Proxy . RegionID ) ,
slog . F ( "proxy_computed_region_id" , regionID ) ,
slog . F ( "proxy_computed_region_code" , regionCode ) ,
)
}
continue statusLoop
}
}
derpMap . Regions [ regionID ] = & tailcfg . DERPRegion {
// EmbeddedRelay ONLY applies to the primary.
EmbeddedRelay : false ,
RegionID : regionID ,
RegionCode : regionCode ,
2023-08-02 03:18:46 +00:00
RegionName : regionName ,
2023-08-10 19:04:17 +00:00
Nodes : [ ] * tailcfg . DERPNode {
{
Name : fmt . Sprintf ( "%da" , regionID ) ,
RegionID : regionID ,
HostName : u . Hostname ( ) ,
DERPPort : portInt ,
STUNPort : - 1 ,
ForceHTTP : u . Scheme == "http" ,
} ,
} ,
2023-07-26 16:21:04 +00:00
}
}
return derpMap
}
}
2023-01-11 15:05:42 +00:00
// @Summary Get entitlements
// @ID get-entitlements
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Success 200 {object} codersdk.Entitlements
// @Router /entitlements [get]
2022-09-20 04:11:01 +00:00
func ( api * API ) serveEntitlements ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-09-20 04:11:01 +00:00
api . entitlementsMu . RLock ( )
entitlements := api . entitlements
api . entitlementsMu . RUnlock ( )
2022-10-07 00:28:22 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , entitlements )
2022-09-20 04:11:01 +00:00
}
func ( api * API ) runEntitlementsLoop ( ctx context . Context ) {
eb := backoff . NewExponentialBackOff ( )
eb . MaxElapsedTime = 0 // retry indefinitely
b := backoff . WithContext ( eb , ctx )
updates := make ( chan struct { } , 1 )
subscribed := false
2023-08-22 14:26:43 +00:00
defer func ( ) {
2023-08-22 18:32:37 +00:00
// If this function ends, it means the context was canceled and this
2023-08-22 14:26:43 +00:00
// coderd is shutting down. In this case, post a pubsub message to
// tell other coderd's to resync their entitlements. This is required to
// make sure things like replica counts are updated in the UI.
// Ignore the error, as this is just a best effort. If it fails,
// the system will eventually recover as replicas timeout
// if their heartbeats stop. The best effort just tries to update the
// UI faster if it succeeds.
_ = api . Pubsub . Publish ( PubsubEventLicenses , [ ] byte ( "going away" ) )
} ( )
2022-09-20 04:11:01 +00:00
for {
select {
case <- ctx . Done ( ) :
return
default :
// pass
}
if ! subscribed {
cancel , err := api . Pubsub . Subscribe ( PubsubEventLicenses , func ( _ context . Context , _ [ ] byte ) {
// don't block. If the channel is full, drop the event, as there is a resync
// scheduled already.
select {
case updates <- struct { } { } :
// pass
default :
// pass
}
} )
if err != nil {
api . Logger . Warn ( ctx , "failed to subscribe to license updates" , slog . Error ( err ) )
select {
case <- ctx . Done ( ) :
return
case <- time . After ( b . NextBackOff ( ) ) :
}
continue
}
// nolint: revive
defer cancel ( )
subscribed = true
api . Logger . Debug ( ctx , "successfully subscribed to pubsub" )
}
2022-09-24 19:55:17 +00:00
api . Logger . Debug ( ctx , "syncing licensed entitlements" )
2022-09-20 04:11:01 +00:00
err := api . updateEntitlements ( ctx )
if err != nil {
api . Logger . Warn ( ctx , "failed to get feature entitlements" , slog . Error ( err ) )
time . Sleep ( b . NextBackOff ( ) )
continue
}
b . Reset ( )
api . Logger . Debug ( ctx , "synced licensed entitlements" )
select {
case <- ctx . Done ( ) :
return
case <- time . After ( api . EntitlementsUpdateInterval ) :
continue
case <- updates :
api . Logger . Debug ( ctx , "got pubsub update" )
continue
}
}
}
2022-10-10 20:37:06 +00:00
func ( api * API ) Authorize ( r * http . Request , action rbac . Action , object rbac . Objecter ) bool {
return api . AGPL . HTTPAuth . Authorize ( r , action , object )
}