Thanks to visit codestin.com
Credit goes to github.com

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

29 files changed

+666
-50
lines changed

cli/clitest/clitest.go

+1-1
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

+38-29
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

+7-7
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

+1-1
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

+20-3
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

+13
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

+5-1
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

+20-1
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

+6-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
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+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- No valid licenses should exist, but to be sure, drop all rows
2+
DELETE FROM licenses;
3+
ALTER TABLE licenses DROP COLUMN license;
4+
ALTER TABLE licenses RENAME COLUMN created_at to uploaded_at;
5+
ALTER TABLE licenses ADD COLUMN jwt text NOT NULL;
6+
-- prevent adding the same license more than once
7+
ALTER TABLE licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
8+
ALTER TABLE licenses ADD COLUMN exp timestamp with time zone NOT NULL;
9+
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.';
10+

coderd/database/models.go

+4-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)