Skip to content

Commit b101a6f

Browse files
authored
POST license API endpoint (#3570)
* POST license API Signed-off-by: Spike Curtis <[email protected]> * Support interface{} types in generated Typescript Signed-off-by: Spike Curtis <[email protected]> * Disable linting on empty interface any Signed-off-by: Spike Curtis <[email protected]> * Code review updates Signed-off-by: Spike Curtis <[email protected]> * Enforce unique licenses Signed-off-by: Spike Curtis <[email protected]> * Renames from code review Signed-off-by: Spike Curtis <[email protected]> * Code review renames and comments Signed-off-by: Spike Curtis <[email protected]> Signed-off-by: Spike Curtis <[email protected]>
1 parent 85acfdf commit b101a6f

File tree

29 files changed

+666
-50
lines changed

29 files changed

+666
-50
lines changed

cli/clitest/clitest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
// New creates a CLI instance with a configuration pointed to a
2222
// temporary testing directory.
2323
func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
24-
cmd := cli.Root()
24+
cmd := cli.Root(cli.AGPL())
2525
dir := t.TempDir()
2626
root := config.Root(dir)
2727
cmd.SetArgs(append([]string{"--global-config", dir}, args...))

cli/root.go

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/coder/coder/cli/cliflag"
2121
"github.com/coder/coder/cli/cliui"
2222
"github.com/coder/coder/cli/config"
23+
"github.com/coder/coder/coderd"
2324
"github.com/coder/coder/codersdk"
2425
)
2526

@@ -58,7 +59,42 @@ func init() {
5859
cobra.AddTemplateFuncs(templateFunctions)
5960
}
6061

61-
func Root() *cobra.Command {
62+
func Core() []*cobra.Command {
63+
return []*cobra.Command{
64+
configSSH(),
65+
create(),
66+
deleteWorkspace(),
67+
dotfiles(),
68+
gitssh(),
69+
list(),
70+
login(),
71+
logout(),
72+
parameters(),
73+
portForward(),
74+
publickey(),
75+
resetPassword(),
76+
schedules(),
77+
show(),
78+
ssh(),
79+
start(),
80+
state(),
81+
stop(),
82+
templates(),
83+
update(),
84+
users(),
85+
versionCmd(),
86+
wireguardPortForward(),
87+
workspaceAgent(),
88+
features(),
89+
}
90+
}
91+
92+
func AGPL() []*cobra.Command {
93+
all := append(Core(), Server(coderd.New))
94+
return all
95+
}
96+
97+
func Root(subcommands []*cobra.Command) *cobra.Command {
6298
cmd := &cobra.Command{
6399
Use: "coder",
64100
SilenceErrors: true,
@@ -109,34 +145,7 @@ func Root() *cobra.Command {
109145
),
110146
}
111147

112-
cmd.AddCommand(
113-
configSSH(),
114-
create(),
115-
deleteWorkspace(),
116-
dotfiles(),
117-
gitssh(),
118-
list(),
119-
login(),
120-
logout(),
121-
parameters(),
122-
portForward(),
123-
publickey(),
124-
resetPassword(),
125-
schedules(),
126-
server(),
127-
show(),
128-
ssh(),
129-
start(),
130-
state(),
131-
stop(),
132-
templates(),
133-
update(),
134-
users(),
135-
versionCmd(),
136-
wireguardPortForward(),
137-
workspaceAgent(),
138-
features(),
139-
)
148+
cmd.AddCommand(subcommands...)
140149

141150
cmd.SetUsageTemplate(usageTemplate())
142151

cli/server.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import (
6868
)
6969

7070
// nolint:gocyclo
71-
func server() *cobra.Command {
71+
func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
7272
var (
7373
accessURL string
7474
address string
@@ -434,7 +434,7 @@ func server() *cobra.Command {
434434
), promAddress, "prometheus")()
435435
}
436436

437-
coderAPI := coderd.New(options)
437+
coderAPI := newAPI(options)
438438
defer coderAPI.Close()
439439

440440
client := codersdk.New(localURL)
@@ -886,16 +886,16 @@ func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API,
886886
// nolint: revive
887887
func printLogo(cmd *cobra.Command, spooky bool) {
888888
if spooky {
889-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), `▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███
889+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), `▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███
890890
▒██▀ ▀█ ▒██▒ ██▒▒██▀ ██▌▓█ ▀ ▓██ ▒ ██▒
891891
▒▓█ ▄ ▒██░ ██▒░██ █▌▒███ ▓██ ░▄█ ▒
892-
▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄
892+
▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄
893893
▒ ▓███▀ ░░ ████▓▒░░▒████▓ ░▒████▒░██▓ ▒██▒
894894
░ ░▒ ▒ ░░ ▒░▒░▒░ ▒▒▓ ▒ ░░ ▒░ ░░ ▒▓ ░▒▓░
895895
░ ▒ ░ ▒ ▒░ ░ ▒ ▒ ░ ░ ░ ░▒ ░ ▒░
896-
░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
897-
░ ░ ░ ░ ░ ░ ░ ░
898-
░ ░
896+
░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
897+
░ ░ ░ ░ ░ ░ ░ ░
898+
░ ░
899899
`)
900900
return
901901
}

cmd/coder/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
func main() {
1616
rand.Seed(time.Now().UnixMicro())
1717

18-
cmd, err := cli.Root().ExecuteC()
18+
cmd, err := cli.Root(cli.AGPL()).ExecuteC()
1919
if err != nil {
2020
if errors.Is(err, cliui.Canceled) {
2121
os.Exit(1)

coderd/authorize.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Act
2727
return objects, nil
2828
}
2929

30+
type HTTPAuthorizer struct {
31+
Authorizer rbac.Authorizer
32+
Logger slog.Logger
33+
}
34+
3035
// Authorize will return false if the user is not authorized to do the action.
3136
// This function will log appropriately, but the caller must return an
3237
// error to the api client.
@@ -37,14 +42,26 @@ func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Act
3742
// return
3843
// }
3944
func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
45+
return api.httpAuth.Authorize(r, action, object)
46+
}
47+
48+
// Authorize will return false if the user is not authorized to do the action.
49+
// This function will log appropriately, but the caller must return an
50+
// error to the api client.
51+
// Eg:
52+
// if !h.Authorize(...) {
53+
// httpapi.Forbidden(rw)
54+
// return
55+
// }
56+
func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
4057
roles := httpmw.AuthorizationUserRoles(r)
41-
err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object.RBACObject())
58+
err := h.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object.RBACObject())
4259
if err != nil {
4360
// Log the errors for debugging
4461
internalError := new(rbac.UnauthorizedError)
45-
logger := api.Logger
62+
logger := h.Logger
4663
if xerrors.As(err, internalError) {
47-
logger = api.Logger.With(slog.F("internal", internalError.Internal()))
64+
logger = h.Logger.With(slog.F("internal", internalError.Internal()))
4865
}
4966
// Log information for debugging. This will be very helpful
5067
// in the early days

coderd/coderd.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type Options struct {
6666
Telemetry telemetry.Reporter
6767
TURNServer *turnconn.Server
6868
TracerProvider *sdktrace.TracerProvider
69+
LicenseHandler http.Handler
6970
}
7071

7172
// New constructs a Coder API handler.
@@ -92,6 +93,9 @@ func New(options *Options) *API {
9293
if options.PrometheusRegistry == nil {
9394
options.PrometheusRegistry = prometheus.NewRegistry()
9495
}
96+
if options.LicenseHandler == nil {
97+
options.LicenseHandler = licenses()
98+
}
9599

96100
siteCacheDir := options.CacheDir
97101
if siteCacheDir != "" {
@@ -107,6 +111,10 @@ func New(options *Options) *API {
107111
Options: options,
108112
Handler: r,
109113
siteHandler: site.Handler(site.FS(), binFS),
114+
httpAuth: &HTTPAuthorizer{
115+
Authorizer: options.Authorizer,
116+
Logger: options.Logger,
117+
},
110118
}
111119
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgent, 0)
112120
oauthConfigs := &httpmw.OAuth2Configs{
@@ -395,6 +403,10 @@ func New(options *Options) *API {
395403
r.Use(apiKeyMiddleware)
396404
r.Get("/", entitlements)
397405
})
406+
r.Route("/licenses", func(r chi.Router) {
407+
r.Use(apiKeyMiddleware)
408+
r.Mount("/", options.LicenseHandler)
409+
})
398410
})
399411

400412
r.NotFound(compressHandler(http.HandlerFunc(api.siteHandler.ServeHTTP)).ServeHTTP)
@@ -409,6 +421,7 @@ type API struct {
409421
websocketWaitMutex sync.Mutex
410422
websocketWaitGroup sync.WaitGroup
411423
workspaceAgentCache *wsconncache.Cache
424+
httpAuth *HTTPAuthorizer
412425
}
413426

414427
// Close waits for all WebSocket connections to drain before returning.

coderd/coderdtest/coderdtest.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type Options struct {
7373

7474
// IncludeProvisionerD when true means to start an in-memory provisionerD
7575
IncludeProvisionerD bool
76+
APIBuilder func(*coderd.Options) *coderd.API
7677
}
7778

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

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

179183
// We set the handler after server creation for the access URL.
180-
coderAPI := coderd.New(&coderd.Options{
184+
coderAPI := options.APIBuilder(&coderd.Options{
181185
AgentConnectionUpdateFrequency: 150 * time.Millisecond,
182186
// Force a long disconnection timeout to ensure
183187
// agents are not marked as disconnected during slow tests.

coderd/database/databasefake/databasefake.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func New() database.Store {
4242
workspaceBuilds: make([]database.WorkspaceBuild, 0),
4343
workspaceApps: make([]database.WorkspaceApp, 0),
4444
workspaces: make([]database.Workspace, 0),
45+
licenses: make([]database.License, 0),
4546
},
4647
}
4748
}
@@ -92,8 +93,10 @@ type data struct {
9293
workspaceBuilds []database.WorkspaceBuild
9394
workspaceApps []database.WorkspaceApp
9495
workspaces []database.Workspace
96+
licenses []database.License
9597

96-
deploymentID string
98+
deploymentID string
99+
lastLicenseID int32
97100
}
98101

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

2283+
func (q *fakeQuerier) InsertLicense(
2284+
_ context.Context, arg database.InsertLicenseParams) (database.License, error) {
2285+
q.mutex.RLock()
2286+
defer q.mutex.RUnlock()
2287+
2288+
l := database.License{
2289+
ID: q.lastLicenseID + 1,
2290+
UploadedAt: arg.UploadedAt,
2291+
JWT: arg.JWT,
2292+
Exp: arg.Exp,
2293+
}
2294+
q.lastLicenseID = l.ID
2295+
q.licenses = append(q.licenses, l)
2296+
return l, nil
2297+
}
2298+
22802299
func (q *fakeQuerier) GetUserLinkByLinkedID(_ context.Context, id string) (database.UserLink, error) {
22812300
q.mutex.RLock()
22822301
defer q.mutex.RUnlock()

coderd/database/dump.sql

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Valid licenses don't fit into old format, so delete all data
2+
DELETE FROM licenses;
3+
ALTER TABLE licenses DROP COLUMN jwt;
4+
ALTER TABLE licenses RENAME COLUMN uploaded_at to created_at;
5+
ALTER TABLE licenses ADD COLUMN license jsonb NOT NULL;
6+
ALTER TABLE licenses DROP COLUMN exp;
7+

0 commit comments

Comments
 (0)