From 11dc13c66d6bbab0368bbded0725199db41af07e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 20 Sep 2022 18:15:57 +0000 Subject: [PATCH 1/3] feat: Add browser-only connections to Enterprise Fixes #4131. --- coderd/coderd.go | 5 +- coderd/workspaceagents.go | 5 + codersdk/features.go | 7 +- codersdk/workspaceagents.go | 25 ++++- docs/admin/enterprise.md | 1 + enterprise/cli/server.go | 9 +- enterprise/coderd/coderd.go | 28 +++++- enterprise/coderd/coderd_test.go | 20 +++- .../coderd/coderdenttest/coderdenttest.go | 12 ++- enterprise/coderd/licenses.go | 5 +- enterprise/coderd/workspaceagents.go | 21 ++++ enterprise/coderd/workspaceagents_test.go | 97 +++++++++++++++++++ site/src/api/types.ts | 1 + site/src/components/Resources/Resources.tsx | 9 +- site/src/components/Workspace/Workspace.tsx | 3 + .../src/pages/WorkspacePage/WorkspacePage.tsx | 4 + site/src/testHelpers/entities.ts | 4 + 17 files changed, 234 insertions(+), 22 deletions(-) create mode 100644 enterprise/coderd/workspaceagents.go create mode 100644 enterprise/coderd/workspaceagents_test.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 0b3a420b19d10..abe6020c37cb7 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -493,8 +493,9 @@ func New(options *Options) *API { type API struct { *Options - Auditor atomic.Pointer[audit.Auditor] - HTTPAuth *HTTPAuthorizer + Auditor atomic.Pointer[audit.Auditor] + WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool] + HTTPAuth *HTTPAuthorizer // APIHandler serves "/api/v2" APIHandler chi.Router diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 57c90c5b84479..7e1182a982059 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -393,6 +393,11 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R httpapi.ResourceNotFound(rw) return } + // This is used by Enterprise code to control the functionality of this route. + override := api.WorkspaceClientCoordinateOverride.Load() + if override != nil && (*override)(rw) { + return + } api.websocketWaitMutex.Lock() api.websocketWaitGroup.Add(1) diff --git a/codersdk/features.go b/codersdk/features.go index 37b0113c37dfb..075d5eeff45e8 100644 --- a/codersdk/features.go +++ b/codersdk/features.go @@ -15,11 +15,12 @@ const ( ) const ( - FeatureUserLimit = "user_limit" - FeatureAuditLog = "audit_log" + FeatureUserLimit = "user_limit" + FeatureAuditLog = "audit_log" + FeatureBrowserOnly = "browser_only" ) -var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog} +var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog, FeatureBrowserOnly} type Feature struct { Entitlement Entitlement `json:"entitlement"` diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 2117de03c6ce3..1ab68f601403f 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -270,12 +270,14 @@ func (c *Client) DialWorkspaceAgentTailnet(ctx context.Context, logger slog.Logg } ctx, cancelFunc := context.WithCancel(ctx) closed := make(chan struct{}) + first := make(chan error) go func() { defer close(closed) + isFirst := true for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(ctx); { logger.Debug(ctx, "connecting") // nolint:bodyclose - ws, _, err := websocket.Dial(ctx, coordinateURL.String(), &websocket.DialOptions{ + ws, res, err := websocket.Dial(ctx, coordinateURL.String(), &websocket.DialOptions{ HTTPClient: httpClient, // Need to disable compression to avoid a data-race. CompressionMode: websocket.CompressionDisabled, @@ -284,11 +286,22 @@ func (c *Client) DialWorkspaceAgentTailnet(ctx context.Context, logger slog.Logg _ = ws.Close(websocket.StatusAbnormalClosure, "") return } + if isFirst { + if res.StatusCode == http.StatusConflict { + first <- readBodyAsError(res) + return + } + isFirst = false + close(first) + } if err != nil { logger.Debug(ctx, "failed to dial", slog.Error(err)) - _ = ws.Close(websocket.StatusAbnormalClosure, "") continue } + if isFirst { + isFirst = false + close(first) + } sendNode, errChan := tailnet.ServeCoordinator(websocket.NetConn(ctx, ws, websocket.MessageBinary), func(node []*tailnet.Node) error { return conn.UpdateNodes(node) }) @@ -307,13 +320,19 @@ func (c *Client) DialWorkspaceAgentTailnet(ctx context.Context, logger slog.Logg _ = ws.Close(websocket.StatusAbnormalClosure, "") } }() + err = <-first + if err != nil { + cancelFunc() + _ = conn.Close() + return nil, err + } return &agent.Conn{ Conn: conn, CloseFunc: func() { cancelFunc() <-closed }, - }, nil + }, err } // WorkspaceAgent returns an agent by ID. diff --git a/docs/admin/enterprise.md b/docs/admin/enterprise.md index 881a6e8b6f8aa..6fbe2109d00ce 100644 --- a/docs/admin/enterprise.md +++ b/docs/admin/enterprise.md @@ -6,6 +6,7 @@ Contact sales@coder.com to obtain a license. These features are: * Audit Logging + * Browser Only Connections ## Adding your license key diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index 8fd9e542af4cc..a35546235cbe4 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" "github.com/coder/coder/cli/cliflag" + "github.com/coder/coder/cli/cliui" "github.com/coder/coder/enterprise/coderd" agpl "github.com/coder/coder/cli" @@ -15,10 +16,12 @@ import ( func server() *cobra.Command { var ( auditLogging bool + browserOnly bool ) cmd := agpl.Server(func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, error) { api, err := coderd.New(ctx, &coderd.Options{ AuditLogging: auditLogging, + BrowserOnly: browserOnly, Options: options, }) if err != nil { @@ -26,8 +29,12 @@ func server() *cobra.Command { } return api.AGPL, nil }) + enterpriseOnly := cliui.Styles.Keyword.Render("This is an Enterprise feature. Contact sales@coder.com for licensing") + cliflag.BoolVarP(cmd.Flags(), &auditLogging, "audit-logging", "", "CODER_AUDIT_LOGGING", true, - "Specifies whether audit logging is enabled.") + "Specifies whether audit logging is enabled. "+enterpriseOnly) + cliflag.BoolVarP(cmd.Flags(), &browserOnly, "browser-only", "", "CODER_BROWSER_ONLY", false, + "Whether Coder only allows connections to workspaces via the browser. "+enterpriseOnly) return cmd } diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 20140f0e80d83..f7005b1fe072a 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -52,7 +52,6 @@ func New(ctx context.Context, options *Options) (*API, error) { OIDC: options.OIDCConfig, } apiKeyMiddleware := httpmw.ExtractAPIKey(options.Database, oauthConfigs, false) - api.AGPL.APIHandler.Group(func(r chi.Router) { r.Get("/entitlements", api.serveEntitlements) r.Route("/licenses", func(r chi.Router) { @@ -75,7 +74,9 @@ func New(ctx context.Context, options *Options) (*API, error) { type Options struct { *coderd.Options - AuditLogging bool + AuditLogging bool + // Whether to block non-browser connections. + BrowserOnly bool EntitlementsUpdateInterval time.Duration Keys map[string]ed25519.PublicKey } @@ -93,6 +94,7 @@ type entitlements struct { hasLicense bool activeUsers codersdk.Feature auditLogs codersdk.Entitlement + browserOnly codersdk.Entitlement } func (api *API) Close() error { @@ -149,13 +151,16 @@ func (api *API) updateEntitlements(ctx context.Context) error { if claims.Features.AuditLog > 0 { entitlements.auditLogs = entitlement } + if claims.Features.BrowserOnly > 0 { + entitlements.browserOnly = entitlement + } } if entitlements.auditLogs != api.entitlements.auditLogs { auditor := agplaudit.NewNop() // A flag could be added to the options that would allow disabling // enhanced audit logging here! - if entitlements.auditLogs == codersdk.EntitlementEntitled && api.AuditLogging { + if entitlements.auditLogs != codersdk.EntitlementNotEntitled && api.AuditLogging { auditor = audit.NewAuditor( audit.DefaultFilter, backends.NewPostgres(api.Database, true), @@ -165,6 +170,14 @@ func (api *API) updateEntitlements(ctx context.Context) error { api.AGPL.Auditor.Store(&auditor) } + if entitlements.browserOnly != api.entitlements.browserOnly { + var handler func(rw http.ResponseWriter) bool + if entitlements.browserOnly != codersdk.EntitlementNotEntitled && api.BrowserOnly { + handler = api.shouldBlockNonBrowserConnections + } + api.AGPL.WorkspaceClientCoordinateOverride.Store(&handler) + } + api.entitlements = entitlements return nil @@ -210,6 +223,15 @@ func (api *API) serveEntitlements(rw http.ResponseWriter, r *http.Request) { "Audit logging is enabled but your license for this feature is expired.") } + resp.Features[codersdk.FeatureBrowserOnly] = codersdk.Feature{ + Entitlement: entitlements.browserOnly, + Enabled: api.BrowserOnly, + } + if entitlements.browserOnly == codersdk.EntitlementGracePeriod && api.BrowserOnly { + resp.Warnings = append(resp.Warnings, + "Browser only connections are enabled but your license for this feature is expired.") + } + httpapi.Write(rw, http.StatusOK, resp) } diff --git a/enterprise/coderd/coderd_test.go b/enterprise/coderd/coderd_test.go index aedd79417be41..4f5d47ec56a23 100644 --- a/enterprise/coderd/coderd_test.go +++ b/enterprise/coderd/coderd_test.go @@ -84,15 +84,18 @@ func TestEntitlements(t *testing.T) { }) t.Run("Warnings", func(t *testing.T) { t.Parallel() - client := coderdenttest.New(t, nil) + client := coderdenttest.New(t, &coderdenttest.Options{ + BrowserOnly: true, + }) first := coderdtest.CreateFirstUser(t, client) for i := 0; i < 4; i++ { coderdtest.CreateAnotherUser(t, client, first.OrganizationID) } coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - UserLimit: 4, - AuditLog: true, - GraceAt: time.Now().Add(-time.Second), + UserLimit: 4, + AuditLog: true, + BrowserOnly: true, + GraceAt: time.Now().Add(-time.Second), }) res, err := client.Entitlements(context.Background()) require.NoError(t, err) @@ -107,11 +110,18 @@ func TestEntitlements(t *testing.T) { assert.True(t, al.Enabled) assert.Nil(t, al.Limit) assert.Nil(t, al.Actual) - assert.Len(t, res.Warnings, 2) + bo := res.Features[codersdk.FeatureBrowserOnly] + assert.Equal(t, codersdk.EntitlementGracePeriod, bo.Entitlement) + assert.True(t, bo.Enabled) + assert.Nil(t, bo.Limit) + assert.Nil(t, bo.Actual) + assert.Len(t, res.Warnings, 3) assert.Contains(t, res.Warnings, "Your deployment has 5 active users but is only licensed for 4.") assert.Contains(t, res.Warnings, "Audit logging is enabled but your license for this feature is expired.") + assert.Contains(t, res.Warnings, + "Browser only connections are enabled but your license for this feature is expired.") }) t.Run("Pubsub", func(t *testing.T) { t.Parallel() diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index 572b858bea31f..faddf00064ed9 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -36,6 +36,7 @@ func init() { type Options struct { *coderdtest.Options + BrowserOnly bool EntitlementsUpdateInterval time.Duration } @@ -55,6 +56,7 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c srv, cancelFunc, oop := coderdtest.NewOptions(t, options.Options) coderAPI, err := coderd.New(context.Background(), &coderd.Options{ AuditLogging: true, + BrowserOnly: options.BrowserOnly, Options: oop, EntitlementsUpdateInterval: options.EntitlementsUpdateInterval, Keys: map[string]ed25519.PublicKey{ @@ -82,6 +84,7 @@ type LicenseOptions struct { ExpiresAt time.Time UserLimit int64 AuditLog bool + BrowserOnly bool } // AddLicense generates a new license with the options provided and inserts it. @@ -105,6 +108,10 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { if options.AuditLog { auditLog = 1 } + browserOnly := int64(0) + if options.BrowserOnly { + browserOnly = 1 + } c := &coderd.Claims{ RegisteredClaims: jwt.RegisteredClaims{ Issuer: "test@testing.test", @@ -117,8 +124,9 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { AccountID: options.AccountID, Version: coderd.CurrentVersion, Features: coderd.Features{ - UserLimit: options.UserLimit, - AuditLog: auditLog, + UserLimit: options.UserLimit, + AuditLog: auditLog, + BrowserOnly: browserOnly, }, } tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, c) diff --git a/enterprise/coderd/licenses.go b/enterprise/coderd/licenses.go index 5b8273f2ffe60..39a23a851e622 100644 --- a/enterprise/coderd/licenses.go +++ b/enterprise/coderd/licenses.go @@ -45,8 +45,9 @@ var key20220812 []byte var Keys = map[string]ed25519.PublicKey{"2022-08-12": ed25519.PublicKey(key20220812)} type Features struct { - UserLimit int64 `json:"user_limit"` - AuditLog int64 `json:"audit_log"` + UserLimit int64 `json:"user_limit"` + AuditLog int64 `json:"audit_log"` + BrowserOnly int64 `json:"browser_only"` } type Claims struct { diff --git a/enterprise/coderd/workspaceagents.go b/enterprise/coderd/workspaceagents.go new file mode 100644 index 0000000000000..a6cd99da1de0b --- /dev/null +++ b/enterprise/coderd/workspaceagents.go @@ -0,0 +1,21 @@ +package coderd + +import ( + "net/http" + + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/codersdk" +) + +func (api *API) shouldBlockNonBrowserConnections(rw http.ResponseWriter) bool { + api.entitlementsMu.Lock() + browserOnly := api.entitlements.browserOnly + api.entitlementsMu.Unlock() + if api.BrowserOnly && browserOnly != codersdk.EntitlementNotEntitled { + httpapi.Write(rw, http.StatusConflict, codersdk.Response{ + Message: "Non-browser connections are disabled for your deployment.", + }) + return true + } + return false +} diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go new file mode 100644 index 0000000000000..5b7c5b93b601d --- /dev/null +++ b/enterprise/coderd/workspaceagents_test.go @@ -0,0 +1,97 @@ +package coderd_test + +import ( + "context" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/agent" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" +) + +func TestBlockNonBrowser(t *testing.T) { + t.Parallel() + t.Run("Enabled", func(t *testing.T) { + t.Parallel() + client := coderdenttest.New(t, &coderdenttest.Options{ + BrowserOnly: true, + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + }) + user := coderdtest.CreateFirstUser(t, client) + coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ + BrowserOnly: true, + }) + id := setupWorkspaceAgent(t, client, user) + _, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, id) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + t.Run("Disabled", func(t *testing.T) { + t.Parallel() + client := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + }) + user := coderdtest.CreateFirstUser(t, client) + coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ + BrowserOnly: false, + }) + id := setupWorkspaceAgent(t, client, user) + conn, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, id) + require.NoError(t, err) + _ = conn.Close() + }) +} + +func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse) uuid.UUID { + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Name: "example", + Auth: &proto.Agent_Token{ + Token: authToken, + }, + }}, + }}, + }, + }, + }}, + }) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + agentCloser := agent.New(agent.Options{ + FetchMetadata: agentClient.WorkspaceAgentMetadata, + CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet, + Logger: slogtest.Make(t, nil).Named("agent"), + }) + defer func() { + _ = agentCloser.Close() + }() + resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + return resources[0].Agents[0].ID +} diff --git a/site/src/api/types.ts b/site/src/api/types.ts index 1ac58f28cfccc..63d01a0ab55dd 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -19,4 +19,5 @@ export type Message = { message: string } export enum FeatureNames { AuditLog = "audit_log", UserLimit = "user_limit", + BrowserOnly = "browser_only", } diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index 4f90a3a622c6a..d935f2096d1bc 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -41,6 +41,7 @@ interface ResourcesProps { workspace: Workspace canUpdateWorkspace: boolean buildInfo?: BuildInfoResponse | undefined + hideSSHButton?: boolean } export const Resources: FC> = ({ @@ -49,6 +50,7 @@ export const Resources: FC> = ({ workspace, canUpdateWorkspace, buildInfo, + hideSSHButton, }) => { const styles = useStyles() const theme: Theme = useTheme() @@ -149,7 +151,12 @@ export const Resources: FC> = ({
{canUpdateWorkspace && agent.status === "connected" && ( <> - + {!hideSSHButton && ( + + )} > buildInfo?: TypesGen.BuildInfoResponse } @@ -63,6 +64,7 @@ export const Workspace: FC> = ({ builds, canUpdateWorkspace, workspaceErrors, + hideSSHButton, buildInfo, }) => { const styles = useStyles() @@ -137,6 +139,7 @@ export const Workspace: FC> = ({ workspace={workspace} canUpdateWorkspace={canUpdateWorkspace} buildInfo={buildInfo} + hideSSHButton={hideSSHButton} /> )} diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index eaf9ce1117efb..9ebcb012e6524 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -14,10 +14,12 @@ import { firstOrItem } from "../../util/array" import { pageTitle } from "../../util/page" import { canExtendDeadline, canReduceDeadline, maxDeadline, minDeadline } from "../../util/schedule" import { getFaviconByStatus } from "../../util/workspace" +import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { selectUser } from "../../xServices/auth/authSelectors" import { XServiceContext } from "../../xServices/StateContext" import { workspaceMachine } from "../../xServices/workspace/workspaceXService" import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService" +import { FeatureNames } from "api/types" dayjs.extend(minMax) @@ -30,6 +32,7 @@ export const WorkspacePage: FC = () => { const xServices = useContext(XServiceContext) const me = useSelector(xServices.authXService, selectUser) + const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility) const [workspaceState, workspaceSend] = useMachine(workspaceMachine, { context: { @@ -131,6 +134,7 @@ export const WorkspacePage: FC = () => { resources={resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} + hideSSHButton={featureVisibility[FeatureNames.BrowserOnly]} workspaceErrors={{ [WorkspaceErrors.GET_RESOURCES_ERROR]: getResourcesError, [WorkspaceErrors.GET_BUILDS_ERROR]: getBuildsError, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 598d37753a71f..03be35aa9e4bb 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -768,6 +768,10 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { enabled: true, entitlement: "entitled", }, + browser_only: { + enabled: true, + entitlement: "entitled", + }, }, } From c89179b92fcccc1c4f5e1a0788b3e6468e7f97dc Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 21 Sep 2022 21:44:53 +0000 Subject: [PATCH 2/3] Fix formatting --- site/src/pages/WorkspacePage/WorkspacePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index b980e47a6cc24..6b243aaa500a2 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,11 +1,13 @@ import { makeStyles } from "@material-ui/core/styles" import { useActor, useMachine, useSelector } from "@xstate/react" +import { FeatureNames } from "api/types" import dayjs from "dayjs" import minMax from "dayjs/plugin/minMax" import { FC, useContext, useEffect } from "react" import { Helmet } from "react-helmet-async" import { useTranslation } from "react-i18next" import { useParams } from "react-router-dom" +import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { DeleteDialog } from "../../components/Dialogs/DeleteDialog/DeleteDialog" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" @@ -14,12 +16,10 @@ import { firstOrItem } from "../../util/array" import { pageTitle } from "../../util/page" import { canExtendDeadline, canReduceDeadline, maxDeadline, minDeadline } from "../../util/schedule" import { getFaviconByStatus } from "../../util/workspace" -import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { selectUser } from "../../xServices/auth/authSelectors" import { XServiceContext } from "../../xServices/StateContext" import { workspaceMachine } from "../../xServices/workspace/workspaceXService" import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService" -import { FeatureNames } from "api/types" dayjs.extend(minMax) From d77cb80a89ff3aba97d454175b913d64e194f387 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 22 Sep 2022 02:51:38 +0000 Subject: [PATCH 3/3] Fix context --- enterprise/cli/features_test.go | 4 +++- enterprise/coderd/coderd.go | 5 +++-- enterprise/coderd/licenses_test.go | 30 ++++++++++++++++------------ enterprise/coderd/workspaceagents.go | 3 ++- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/enterprise/cli/features_test.go b/enterprise/cli/features_test.go index 7f7d13a5180d6..4832082e28c32 100644 --- a/enterprise/cli/features_test.go +++ b/enterprise/cli/features_test.go @@ -57,12 +57,14 @@ func TestFeaturesList(t *testing.T) { var entitlements codersdk.Entitlements err := json.Unmarshal(buf.Bytes(), &entitlements) require.NoError(t, err, "unmarshal JSON output") - assert.Len(t, entitlements.Features, 2) + assert.Len(t, entitlements.Features, 3) assert.Empty(t, entitlements.Warnings) assert.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[codersdk.FeatureUserLimit].Entitlement) assert.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement) + assert.Equal(t, codersdk.EntitlementNotEntitled, + entitlements.Features[codersdk.FeatureBrowserOnly].Entitlement) assert.False(t, entitlements.HasLicense) }) } diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 087fd8a7f4bcb..237f15ebdcda5 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -133,8 +133,9 @@ func (api *API) updateEntitlements(ctx context.Context) error { Enabled: false, Entitlement: codersdk.EntitlementNotEntitled, }, - auditLogs: codersdk.EntitlementNotEntitled, - scim: codersdk.EntitlementNotEntitled, + auditLogs: codersdk.EntitlementNotEntitled, + scim: codersdk.EntitlementNotEntitled, + browserOnly: codersdk.EntitlementNotEntitled, } // Here we loop through licenses to detect enabled features. diff --git a/enterprise/coderd/licenses_test.go b/enterprise/coderd/licenses_test.go index 12fa82583436e..0b74c1caf6239 100644 --- a/enterprise/coderd/licenses_test.go +++ b/enterprise/coderd/licenses_test.go @@ -78,16 +78,18 @@ func TestGetLicense(t *testing.T) { defer cancel() coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - AccountID: "testing", - AuditLog: true, - SCIM: true, + AccountID: "testing", + AuditLog: true, + SCIM: true, + BrowserOnly: true, }) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - AccountID: "testing2", - AuditLog: true, - SCIM: true, - UserLimit: 200, + AccountID: "testing2", + AuditLog: true, + SCIM: true, + BrowserOnly: true, + UserLimit: 200, }) licenses, err := client.Licenses(ctx) @@ -96,16 +98,18 @@ func TestGetLicense(t *testing.T) { assert.Equal(t, int32(1), licenses[0].ID) assert.Equal(t, "testing", licenses[0].Claims["account_id"]) assert.Equal(t, map[string]interface{}{ - codersdk.FeatureUserLimit: json.Number("0"), - codersdk.FeatureAuditLog: json.Number("1"), - codersdk.FeatureSCIM: json.Number("1"), + codersdk.FeatureUserLimit: json.Number("0"), + codersdk.FeatureAuditLog: json.Number("1"), + codersdk.FeatureSCIM: json.Number("1"), + codersdk.FeatureBrowserOnly: json.Number("1"), }, licenses[0].Claims["features"]) assert.Equal(t, int32(2), licenses[1].ID) assert.Equal(t, "testing2", licenses[1].Claims["account_id"]) assert.Equal(t, map[string]interface{}{ - codersdk.FeatureUserLimit: json.Number("200"), - codersdk.FeatureAuditLog: json.Number("1"), - codersdk.FeatureSCIM: json.Number("1"), + codersdk.FeatureUserLimit: json.Number("200"), + codersdk.FeatureAuditLog: json.Number("1"), + codersdk.FeatureSCIM: json.Number("1"), + codersdk.FeatureBrowserOnly: json.Number("1"), }, licenses[1].Claims["features"]) }) } diff --git a/enterprise/coderd/workspaceagents.go b/enterprise/coderd/workspaceagents.go index a6cd99da1de0b..def912a90f893 100644 --- a/enterprise/coderd/workspaceagents.go +++ b/enterprise/coderd/workspaceagents.go @@ -1,6 +1,7 @@ package coderd import ( + "context" "net/http" "github.com/coder/coder/coderd/httpapi" @@ -12,7 +13,7 @@ func (api *API) shouldBlockNonBrowserConnections(rw http.ResponseWriter) bool { browserOnly := api.entitlements.browserOnly api.entitlementsMu.Unlock() if api.BrowserOnly && browserOnly != codersdk.EntitlementNotEntitled { - httpapi.Write(rw, http.StatusConflict, codersdk.Response{ + httpapi.Write(context.Background(), rw, http.StatusConflict, codersdk.Response{ Message: "Non-browser connections are disabled for your deployment.", }) return true