Skip to content

Commit 30c4b4d

Browse files
authored
chore: implement fetch all authorized templates api (coder#13678)
1 parent 08e728b commit 30c4b4d

File tree

7 files changed

+315
-54
lines changed

7 files changed

+315
-54
lines changed

coderd/apidoc/docs.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ func New(options *Options) *API {
827827
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
828828
r.Route("/templates", func(r chi.Router) {
829829
r.Post("/", api.postTemplateByOrganization)
830-
r.Get("/", api.templatesByOrganization)
830+
r.Get("/", api.templatesByOrganization())
831831
r.Get("/examples", api.templateExamples)
832832
r.Route("/{templatename}", func(r chi.Router) {
833833
r.Get("/", api.templateByOrganizationAndName)
@@ -869,20 +869,25 @@ func New(options *Options) *API {
869869
})
870870
})
871871
})
872-
r.Route("/templates/{template}", func(r chi.Router) {
872+
r.Route("/templates", func(r chi.Router) {
873873
r.Use(
874874
apiKeyMiddleware,
875-
httpmw.ExtractTemplateParam(options.Database),
876875
)
877-
r.Get("/daus", api.templateDAUs)
878-
r.Get("/", api.template)
879-
r.Delete("/", api.deleteTemplate)
880-
r.Patch("/", api.patchTemplateMeta)
881-
r.Route("/versions", func(r chi.Router) {
882-
r.Post("/archive", api.postArchiveTemplateVersions)
883-
r.Get("/", api.templateVersionsByTemplate)
884-
r.Patch("/", api.patchActiveTemplateVersion)
885-
r.Get("/{templateversionname}", api.templateVersionByName)
876+
r.Get("/", api.fetchTemplates(nil))
877+
r.Route("/{template}", func(r chi.Router) {
878+
r.Use(
879+
httpmw.ExtractTemplateParam(options.Database),
880+
)
881+
r.Get("/daus", api.templateDAUs)
882+
r.Get("/", api.template)
883+
r.Delete("/", api.deleteTemplate)
884+
r.Patch("/", api.patchTemplateMeta)
885+
r.Route("/versions", func(r chi.Router) {
886+
r.Post("/archive", api.postArchiveTemplateVersions)
887+
r.Get("/", api.templateVersionsByTemplate)
888+
r.Patch("/", api.patchActiveTemplateVersion)
889+
r.Get("/{templateversionname}", api.templateVersionByName)
890+
})
886891
})
887892
})
888893
r.Route("/templateversions/{templateversion}", func(r chi.Router) {

coderd/templates.go

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -435,55 +435,78 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
435435
// @Param organization path string true "Organization ID" format(uuid)
436436
// @Success 200 {array} codersdk.Template
437437
// @Router /organizations/{organization}/templates [get]
438-
func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request) {
439-
ctx := r.Context()
440-
organization := httpmw.OrganizationParam(r)
438+
func (api *API) templatesByOrganization() http.HandlerFunc {
439+
// TODO: Should deprecate this endpoint and make it akin to /workspaces with
440+
// a filter. There isn't a need to make the organization filter argument
441+
// part of the query url.
442+
// mutate the filter to only include templates from the given organization.
443+
return api.fetchTemplates(func(r *http.Request, arg *database.GetTemplatesWithFilterParams) {
444+
organization := httpmw.OrganizationParam(r)
445+
arg.OrganizationID = organization.ID
446+
})
447+
}
441448

442-
p := httpapi.NewQueryParamParser()
443-
values := r.URL.Query()
449+
// @Summary Get all templates
450+
// @ID get-all-templates
451+
// @Security CoderSessionToken
452+
// @Produce json
453+
// @Tags Templates
454+
// @Success 200 {array} codersdk.Template
455+
// @Router /templates [get]
456+
func (api *API) fetchTemplates(mutate func(r *http.Request, arg *database.GetTemplatesWithFilterParams)) http.HandlerFunc {
457+
return func(rw http.ResponseWriter, r *http.Request) {
458+
ctx := r.Context()
459+
460+
p := httpapi.NewQueryParamParser()
461+
values := r.URL.Query()
462+
463+
deprecated := sql.NullBool{}
464+
if values.Has("deprecated") {
465+
deprecated = sql.NullBool{
466+
Bool: p.Boolean(values, false, "deprecated"),
467+
Valid: true,
468+
}
469+
}
470+
if len(p.Errors) > 0 {
471+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
472+
Message: "Invalid query params.",
473+
Validations: p.Errors,
474+
})
475+
return
476+
}
444477

445-
deprecated := sql.NullBool{}
446-
if values.Has("deprecated") {
447-
deprecated = sql.NullBool{
448-
Bool: p.Boolean(values, false, "deprecated"),
449-
Valid: true,
478+
prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type)
479+
if err != nil {
480+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
481+
Message: "Internal error preparing sql filter.",
482+
Detail: err.Error(),
483+
})
484+
return
450485
}
451-
}
452-
if len(p.Errors) > 0 {
453-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
454-
Message: "Invalid query params.",
455-
Validations: p.Errors,
456-
})
457-
return
458-
}
459486

460-
prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type)
461-
if err != nil {
462-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
463-
Message: "Internal error preparing sql filter.",
464-
Detail: err.Error(),
465-
})
466-
return
467-
}
487+
args := database.GetTemplatesWithFilterParams{
488+
Deprecated: deprecated,
489+
}
490+
if mutate != nil {
491+
mutate(r, &args)
492+
}
468493

469-
// Filter templates based on rbac permissions
470-
templates, err := api.Database.GetAuthorizedTemplates(ctx, database.GetTemplatesWithFilterParams{
471-
OrganizationID: organization.ID,
472-
Deprecated: deprecated,
473-
}, prepared)
474-
if errors.Is(err, sql.ErrNoRows) {
475-
err = nil
476-
}
494+
// Filter templates based on rbac permissions
495+
templates, err := api.Database.GetAuthorizedTemplates(ctx, args, prepared)
496+
if errors.Is(err, sql.ErrNoRows) {
497+
err = nil
498+
}
477499

478-
if err != nil {
479-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
480-
Message: "Internal error fetching templates in organization.",
481-
Detail: err.Error(),
482-
})
483-
return
484-
}
500+
if err != nil {
501+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
502+
Message: "Internal error fetching templates in organization.",
503+
Detail: err.Error(),
504+
})
505+
return
506+
}
485507

486-
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates))
508+
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates))
509+
}
487510
}
488511

489512
// @Summary Get templates by organization and template name

coderd/templates_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,42 @@ func TestTemplatesByOrganization(t *testing.T) {
438438
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
439439
require.NoError(t, err)
440440
require.Len(t, templates, 2)
441+
442+
// Listing all should match
443+
templates, err = client.Templates(ctx)
444+
require.NoError(t, err)
445+
require.Len(t, templates, 2)
446+
})
447+
t.Run("MultipleOrganizations", func(t *testing.T) {
448+
t.Parallel()
449+
client := coderdtest.New(t, nil)
450+
owner := coderdtest.CreateFirstUser(t, client)
451+
org2 := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
452+
user, _ := coderdtest.CreateAnotherUser(t, client, org2.ID)
453+
454+
// 2 templates in first organization
455+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
456+
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
457+
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
458+
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version2.ID)
459+
460+
// 2 in the second organization
461+
version3 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil)
462+
version4 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil)
463+
coderdtest.CreateTemplate(t, client, org2.ID, version3.ID)
464+
coderdtest.CreateTemplate(t, client, org2.ID, version4.ID)
465+
466+
ctx := testutil.Context(t, testutil.WaitLong)
467+
468+
// All 4 are viewable by the owner
469+
templates, err := client.Templates(ctx)
470+
require.NoError(t, err)
471+
require.Len(t, templates, 4)
472+
473+
// Only 2 are viewable by the org user
474+
templates, err = user.Templates(ctx)
475+
require.NoError(t, err)
476+
require.Len(t, templates, 2)
441477
})
442478
}
443479

codersdk/organizations.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,25 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui
362362
return templates, json.NewDecoder(res.Body).Decode(&templates)
363363
}
364364

365+
// Templates lists all viewable templates
366+
func (c *Client) Templates(ctx context.Context) ([]Template, error) {
367+
res, err := c.Request(ctx, http.MethodGet,
368+
"/api/v2/templates",
369+
nil,
370+
)
371+
if err != nil {
372+
return nil, xerrors.Errorf("execute request: %w", err)
373+
}
374+
defer res.Body.Close()
375+
376+
if res.StatusCode != http.StatusOK {
377+
return nil, ReadBodyAsError(res)
378+
}
379+
380+
var templates []Template
381+
return templates, json.NewDecoder(res.Body).Decode(&templates)
382+
}
383+
365384
// TemplateByName finds a template inside the organization provided with a case-insensitive name.
366385
func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, name string) (Template, error) {
367386
if name == "" {

0 commit comments

Comments
 (0)