Skip to content

Commit 30c4b4d

Browse files
authored
chore: implement fetch all authorized templates api (#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy