Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/clitest/clitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
// New creates a CLI instance with a configuration pointed to a
// temporary testing directory.
func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
cmd := cli.Root()
cmd := cli.Root(cli.AGPLSubcommands())
dir := t.TempDir()
root := config.Root(dir)
cmd.SetArgs(append([]string{"--global-config", dir}, args...))
Expand Down
67 changes: 38 additions & 29 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/coderd"
"github.com/coder/coder/codersdk"
)

Expand Down Expand Up @@ -58,7 +59,42 @@ func init() {
cobra.AddTemplateFuncs(templateFunctions)
}

func Root() *cobra.Command {
func CoreSubcommands() []*cobra.Command {
return []*cobra.Command{
configSSH(),
create(),
deleteWorkspace(),
dotfiles(),
gitssh(),
list(),
login(),
logout(),
parameters(),
portForward(),
publickey(),
resetPassword(),
schedules(),
show(),
ssh(),
start(),
state(),
stop(),
templates(),
update(),
users(),
versionCmd(),
wireguardPortForward(),
workspaceAgent(),
features(),
}
}

func AGPLSubcommands() []*cobra.Command {
all := append(CoreSubcommands(), Server(coderd.New))
return all
}

func Root(subcommands []*cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "coder",
SilenceErrors: true,
Expand Down Expand Up @@ -109,34 +145,7 @@ func Root() *cobra.Command {
),
}

cmd.AddCommand(
configSSH(),
create(),
deleteWorkspace(),
dotfiles(),
gitssh(),
list(),
login(),
logout(),
parameters(),
portForward(),
publickey(),
resetPassword(),
schedules(),
server(),
show(),
ssh(),
start(),
state(),
stop(),
templates(),
update(),
users(),
versionCmd(),
wireguardPortForward(),
workspaceAgent(),
features(),
)
cmd.AddCommand(subcommands...)

cmd.SetUsageTemplate(usageTemplate())

Expand Down
16 changes: 9 additions & 7 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ import (
"github.com/coder/coder/provisionersdk/proto"
)

type CoderServerBuilder func(*coderd.Options) *coderd.API

// nolint:gocyclo
func server() *cobra.Command {
func Server(builder CoderServerBuilder) *cobra.Command {
var (
accessURL string
address string
Expand Down Expand Up @@ -434,7 +436,7 @@ func server() *cobra.Command {
), promAddress, "prometheus")()
}

coderAPI := coderd.New(options)
coderAPI := builder(options)
defer coderAPI.Close()

client := codersdk.New(localURL)
Expand Down Expand Up @@ -885,16 +887,16 @@ func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API,
// nolint: revive
func printLogo(cmd *cobra.Command, spooky bool) {
if spooky {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), `▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███
_, _ = fmt.Fprintf(cmd.OutOrStdout(), `▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███
▒██▀ ▀█ ▒██▒ ██▒▒██▀ ██▌▓█ ▀ ▓██ ▒ ██▒
▒▓█ ▄ ▒██░ ██▒░██ █▌▒███ ▓██ ░▄█ ▒
▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄
▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄
▒ ▓███▀ ░░ ████▓▒░░▒████▓ ░▒████▒░██▓ ▒██▒
░ ░▒ ▒ ░░ ▒░▒░▒░ ▒▒▓ ▒ ░░ ▒░ ░░ ▒▓ ░▒▓░
░ ▒ ░ ▒ ▒░ ░ ▒ ▒ ░ ░ ░ ░▒ ░ ▒░
░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░
░ ░
░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░
░ ░
`)
return
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/coder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
func main() {
rand.Seed(time.Now().UnixMicro())

cmd, err := cli.Root().ExecuteC()
cmd, err := cli.Root(cli.AGPLSubcommands()).ExecuteC()
if err != nil {
if errors.Is(err, cliui.Canceled) {
os.Exit(1)
Expand Down
23 changes: 20 additions & 3 deletions coderd/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Act
return objects, nil
}

type HTTPAuthorizer struct {
Authorizer rbac.Authorizer
Logger slog.Logger
}

// Authorize will return false if the user is not authorized to do the action.
// This function will log appropriately, but the caller must return an
// error to the api client.
Expand All @@ -36,14 +41,26 @@ func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Act
// return
// }
func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
return api.httpAuth.Authorize(r, action, object)
}

// Authorize will return false if the user is not authorized to do the action.
// This function will log appropriately, but the caller must return an
// error to the api client.
// Eg:
// if !h.Authorize(...) {
// httpapi.Forbidden(rw)
// return
// }
func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
roles := httpmw.AuthorizationUserRoles(r)
err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object.RBACObject())
err := h.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object.RBACObject())
if err != nil {
// Log the errors for debugging
internalError := new(rbac.UnauthorizedError)
logger := api.Logger
logger := h.Logger
if xerrors.As(err, internalError) {
logger = api.Logger.With(slog.F("internal", internalError.Internal()))
logger = h.Logger.With(slog.F("internal", internalError.Internal()))
}
// Log information for debugging. This will be very helpful
// in the early days
Expand Down
13 changes: 13 additions & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type Options struct {
Telemetry telemetry.Reporter
TURNServer *turnconn.Server
TracerProvider *sdktrace.TracerProvider
LicenseHandler http.Handler
}

// New constructs a Coder API handler.
Expand All @@ -92,6 +93,9 @@ func New(options *Options) *API {
if options.PrometheusRegistry == nil {
options.PrometheusRegistry = prometheus.NewRegistry()
}
if options.LicenseHandler == nil {
options.LicenseHandler = Licenses()
}

siteCacheDir := options.CacheDir
if siteCacheDir != "" {
Expand All @@ -107,6 +111,10 @@ func New(options *Options) *API {
Options: options,
Handler: r,
siteHandler: site.Handler(site.FS(), binFS),
httpAuth: &HTTPAuthorizer{
Authorizer: options.Authorizer,
Logger: options.Logger,
},
}
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgent, 0)
oauthConfigs := &httpmw.OAuth2Configs{
Expand Down Expand Up @@ -395,6 +403,10 @@ func New(options *Options) *API {
r.Use(apiKeyMiddleware)
r.Get("/", entitlements)
})
r.Route("/licenses", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Mount("/", options.LicenseHandler)
})
})

r.NotFound(compressHandler(http.HandlerFunc(api.siteHandler.ServeHTTP)).ServeHTTP)
Expand All @@ -409,6 +421,7 @@ type API struct {
websocketWaitMutex sync.Mutex
websocketWaitGroup sync.WaitGroup
workspaceAgentCache *wsconncache.Cache
httpAuth *HTTPAuthorizer
}

// Close waits for all WebSocket connections to drain before returning.
Expand Down
6 changes: 5 additions & 1 deletion coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type Options struct {

// IncludeProvisionerD when true means to start an in-memory provisionerD
IncludeProvisionerD bool
APIBuilder func(*coderd.Options) *coderd.API
}

// New constructs a codersdk client connected to an in-memory API instance.
Expand Down Expand Up @@ -122,6 +123,9 @@ func newWithCloser(t *testing.T, options *Options) (*codersdk.Client, io.Closer)
close(options.AutobuildStats)
})
}
if options.APIBuilder == nil {
options.APIBuilder = coderd.New
}

// This can be hotswapped for a live database instance.
db := databasefake.New()
Expand Down Expand Up @@ -177,7 +181,7 @@ func newWithCloser(t *testing.T, options *Options) (*codersdk.Client, io.Closer)
})

// We set the handler after server creation for the access URL.
coderAPI := coderd.New(&coderd.Options{
coderAPI := options.APIBuilder(&coderd.Options{
AgentConnectionUpdateFrequency: 150 * time.Millisecond,
// Force a long disconnection timeout to ensure
// agents are not marked as disconnected during slow tests.
Expand Down
21 changes: 20 additions & 1 deletion coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func New() database.Store {
workspaceBuilds: make([]database.WorkspaceBuild, 0),
workspaceApps: make([]database.WorkspaceApp, 0),
workspaces: make([]database.Workspace, 0),
licenses: make([]database.License, 0),
},
}
}
Expand Down Expand Up @@ -92,8 +93,10 @@ type data struct {
workspaceBuilds []database.WorkspaceBuild
workspaceApps []database.WorkspaceApp
workspaces []database.Workspace
licenses []database.License

deploymentID string
deploymentID string
lastLicenseID int32
}

// InTx doesn't rollback data properly for in-memory yet.
Expand Down Expand Up @@ -2277,6 +2280,22 @@ func (q *fakeQuerier) GetDeploymentID(_ context.Context) (string, error) {
return q.deploymentID, nil
}

func (q *fakeQuerier) InsertLicense(
_ context.Context, arg database.InsertLicenseParams) (database.License, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()

l := database.License{
ID: q.lastLicenseID + 1,
UploadedAt: arg.UploadedAt,
Jwt: arg.Jwt,
Exp: arg.Exp,
}
q.lastLicenseID = l.ID
q.licenses = append(q.licenses, l)
return l, nil
}

func (q *fakeQuerier) GetUserLinkByLinkedID(_ context.Context, id string) (database.UserLink, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
Expand Down
8 changes: 6 additions & 2 deletions coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions coderd/database/migrations/000037_jwt_licenses.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Valid licenses don't fit into old format, so delete all data
DELETE FROM licenses;
ALTER TABLE licenses DROP COLUMN jwt;
ALTER TABLE licenses RENAME COLUMN uploaded_at to created_at;
ALTER TABLE licenses ADD COLUMN license jsonb NOT NULL;
ALTER TABLE licenses DROP COLUMN exp;

11 changes: 11 additions & 0 deletions coderd/database/migrations/000037_jwt_licenses.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- No valid licenses should exist, but to be sure, drop all rows
DELETE FROM licenses;
ALTER TABLE licenses DROP COLUMN license;
ALTER TABLE licenses RENAME COLUMN created_at to uploaded_at;
ALTER TABLE licenses ADD COLUMN jwt text NOT NULL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In sqlc.go you can add a rename for this column so the output is JWT instead of Jwt.

-- prevent adding the same license more than once
ALTER TABLE licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
-- exp tracks the claim of the same name in the JWT, and we include it here so that we can easily
-- query for licenses that have not yet expired.
ALTER TABLE licenses ADD COLUMN exp timestamp with time zone NOT NULL;

7 changes: 4 additions & 3 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading