Skip to content

POST license API endpoint #3570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all 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.AGPL())
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 Core() []*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 AGPL() []*cobra.Command {
all := append(Core(), 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
14 changes: 7 additions & 7 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import (
)

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

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

client := codersdk.New(localURL)
Expand Down Expand Up @@ -885,16 +885,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.AGPL()).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;

10 changes: 10 additions & 0 deletions coderd/database/migrations/000037_jwt_licenses.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- 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);
ALTER TABLE licenses ADD COLUMN exp timestamp with time zone NOT NULL;
COMMENT ON COLUMN licenses.exp IS '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.';

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