Skip to content

Commit e4f6fd6

Browse files
committed
chore: move app sharing to open source
1 parent a2eacaa commit e4f6fd6

14 files changed

+369
-671
lines changed

coderd/coderd.go

-9
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ type Options struct {
5757

5858
Auditor audit.Auditor
5959
WorkspaceQuotaEnforcer workspacequota.Enforcer
60-
AppAuthorizer AppAuthorizer
6160
AgentConnectionUpdateFrequency time.Duration
6261
AgentInactiveDisconnectTimeout time.Duration
6362
// APIRateLimit is the minutely throughput rate limit per user or ip.
@@ -127,11 +126,6 @@ func New(options *Options) *API {
127126
if options.WorkspaceQuotaEnforcer == nil {
128127
options.WorkspaceQuotaEnforcer = workspacequota.NewNop()
129128
}
130-
if options.AppAuthorizer == nil {
131-
options.AppAuthorizer = &AGPLAppAuthorizer{
132-
RBAC: options.Authorizer,
133-
}
134-
}
135129

136130
siteCacheDir := options.CacheDir
137131
if siteCacheDir != "" {
@@ -160,11 +154,9 @@ func New(options *Options) *API {
160154
metricsCache: metricsCache,
161155
Auditor: atomic.Pointer[audit.Auditor]{},
162156
WorkspaceQuotaEnforcer: atomic.Pointer[workspacequota.Enforcer]{},
163-
AppAuthorizer: atomic.Pointer[AppAuthorizer]{},
164157
}
165158
api.Auditor.Store(&options.Auditor)
166159
api.WorkspaceQuotaEnforcer.Store(&options.WorkspaceQuotaEnforcer)
167-
api.AppAuthorizer.Store(&options.AppAuthorizer)
168160
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0)
169161
api.derpServer = derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger))
170162
oauthConfigs := &httpmw.OAuth2Configs{
@@ -536,7 +528,6 @@ type API struct {
536528
Auditor atomic.Pointer[audit.Auditor]
537529
WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool]
538530
WorkspaceQuotaEnforcer atomic.Pointer[workspacequota.Enforcer]
539-
AppAuthorizer atomic.Pointer[AppAuthorizer]
540531
HTTPAuth *HTTPAuthorizer
541532

542533
// APIHandler serves "/api/v2"

coderd/users.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,7 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
958958
UserID: user.ID,
959959
LoginType: database.LoginTypePassword,
960960
RemoteAddr: r.RemoteAddr,
961-
// All api generated keys will last 1 week. Browser login tokens have
961+
// All API generated keys will last 1 week. Browser login tokens have
962962
// a shorter life.
963963
ExpiresAt: database.Now().Add(lifeTime),
964964
LifetimeSeconds: int64(lifeTime.Seconds()),
@@ -972,7 +972,7 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
972972
}
973973

974974
// We intentionally do not set the cookie on the response here.
975-
// Setting the cookie will couple the browser sesion to the API
975+
// Setting the cookie will couple the browser session to the API
976976
// key we return here, meaning logging out of the website would
977977
// invalid your CLI key.
978978
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: cookie.Value})

coderd/workspaceapps.go

+61-35
Original file line numberDiff line numberDiff line change
@@ -34,42 +34,11 @@ import (
3434
const (
3535
// This needs to be a super unique query parameter because we don't want to
3636
// conflict with query parameters that users may use.
37-
// TODO: this will make dogfooding harder so come up with a more unique
38-
// solution
3937
//nolint:gosec
4038
subdomainProxyAPIKeyParam = "coder_application_connect_api_key_35e783"
4139
redirectURIQueryParam = "redirect_uri"
4240
)
4341

44-
type AppAuthorizer interface {
45-
// Authorize returns true if the request is authorized to access an app at
46-
// share level `AppSharingLevel` in `workspace`. An error is only returned if
47-
// there is a processing error. "Unauthorized" errors should not be
48-
// returned.
49-
//
50-
// It must be able to handle optional user authorization. Use
51-
// `httpmw.*Optional` methods.
52-
Authorize(r *http.Request, db database.Store, AppSharingLevel database.AppSharingLevel, workspace database.Workspace) (bool, error)
53-
}
54-
55-
type AGPLAppAuthorizer struct {
56-
RBAC rbac.Authorizer
57-
}
58-
59-
var _ AppAuthorizer = &AGPLAppAuthorizer{}
60-
61-
// Authorize provides an AGPL implementation of AppAuthorizer. It does not
62-
// support app sharing levels as they are an enterprise feature.
63-
func (a AGPLAppAuthorizer) Authorize(r *http.Request, _ database.Store, _ database.AppSharingLevel, workspace database.Workspace) (bool, error) {
64-
roles, ok := httpmw.UserAuthorizationOptional(r)
65-
if !ok {
66-
return false, nil
67-
}
68-
69-
err := a.RBAC.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC())
70-
return err == nil, nil
71-
}
72-
7342
func (api *API) appHost(rw http.ResponseWriter, r *http.Request) {
7443
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.GetAppHostResponse{
7544
Host: api.AppHostname,
@@ -326,12 +295,69 @@ func (api *API) lookupWorkspaceApp(rw http.ResponseWriter, r *http.Request, agen
326295
return app, true
327296
}
328297

298+
func (api *API) authorizeWorkspaceApp(r *http.Request, sharingLevel database.AppSharingLevel, workspace database.Workspace) (bool, error) {
299+
ctx := r.Context()
300+
301+
// Short circuit if not authenticated.
302+
roles, ok := httpmw.UserAuthorizationOptional(r)
303+
if !ok {
304+
// The user is not authenticated, so they can only access the app if it
305+
// is public.
306+
return sharingLevel == database.AppSharingLevelPublic, nil
307+
}
308+
309+
// Do a standard RBAC check. This accounts for share level "owner" and any
310+
// other RBAC rules that may be in place.
311+
//
312+
// Regardless of share level or whether it's enabled or not, the owner of
313+
// the workspace can always access applications (as long as their key's
314+
// scope allows it).
315+
err := api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC())
316+
if err == nil {
317+
return true, nil
318+
}
319+
320+
switch sharingLevel {
321+
case database.AppSharingLevelOwner:
322+
// We essentially already did this above.
323+
case database.AppSharingLevelTemplate:
324+
// Check if the user has access to the same template as the workspace.
325+
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
326+
if err != nil {
327+
return false, xerrors.Errorf("get template %q: %w", workspace.TemplateID, err)
328+
}
329+
330+
err = api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionRead, template.RBACObject())
331+
if err == nil {
332+
return true, nil
333+
}
334+
case database.AppSharingLevelAuthenticated:
335+
// The user is authenticated at this point, but we need to make sure
336+
// that they have ApplicationConnect permissions to their own
337+
// workspaces. This ensures that the key's scope has permission to
338+
// connect to workspace apps.
339+
object := rbac.ResourceWorkspaceApplicationConnect.WithOwner(roles.ID.String())
340+
err := api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, object)
341+
if err == nil {
342+
return true, nil
343+
}
344+
case database.AppSharingLevelPublic:
345+
// We don't really care about scopes and stuff if it's public anyways.
346+
// Someone with a restricted-scope API key could just not submit the
347+
// API key cookie in the request and access the page.
348+
return true, nil
349+
}
350+
351+
// No checks were successful.
352+
return false, nil
353+
}
354+
329355
// fetchWorkspaceApplicationAuth authorizes the user using api.AppAuthorizer
330356
// for a given app share level in the given workspace. The user's authorization
331357
// status is returned. If a server error occurs, a HTML error page is rendered
332358
// and false is returned so the caller can return early.
333-
func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, AppSharingLevel database.AppSharingLevel) (authed bool, ok bool) {
334-
ok, err := (*api.AppAuthorizer.Load()).Authorize(r, api.Database, AppSharingLevel, workspace)
359+
func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, appSharingLevel database.AppSharingLevel) (authed bool, ok bool) {
360+
ok, err := api.authorizeWorkspaceApp(r, appSharingLevel, workspace)
335361
if err != nil {
336362
api.Logger.Error(r.Context(), "authorize workspace app", slog.Error(err))
337363
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
@@ -351,8 +377,8 @@ func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Re
351377
// for a given app share level in the given workspace. If the user is not
352378
// authorized or a server error occurs, a discrete HTML error page is rendered
353379
// and false is returned so the caller can return early.
354-
func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, AppSharingLevel database.AppSharingLevel) bool {
355-
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, AppSharingLevel)
380+
func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, appSharingLevel database.AppSharingLevel) bool {
381+
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel)
356382
if !ok {
357383
return false
358384
}

0 commit comments

Comments
 (0)