chore: implement fetch all authorized templates api (#13678)

This commit is contained in:
Steven Masley
2024-06-26 07:50:32 -10:00
committed by GitHub
parent 08e728bcb2
commit 30c4b4db5c
7 changed files with 315 additions and 54 deletions

28
coderd/apidoc/docs.go generated
View File

@ -3101,6 +3101,34 @@ const docTemplate = `{
}
}
},
"/templates": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Templates"
],
"summary": "Get all templates",
"operationId": "get-all-templates",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Template"
}
}
}
}
}
},
"/templates/{template}": {
"get": {
"security": [

View File

@ -2725,6 +2725,30 @@
}
}
},
"/templates": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Templates"],
"summary": "Get all templates",
"operationId": "get-all-templates",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Template"
}
}
}
}
}
},
"/templates/{template}": {
"get": {
"security": [

View File

@ -827,7 +827,7 @@ func New(options *Options) *API {
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
r.Route("/templates", func(r chi.Router) {
r.Post("/", api.postTemplateByOrganization)
r.Get("/", api.templatesByOrganization)
r.Get("/", api.templatesByOrganization())
r.Get("/examples", api.templateExamples)
r.Route("/{templatename}", func(r chi.Router) {
r.Get("/", api.templateByOrganizationAndName)
@ -869,20 +869,25 @@ func New(options *Options) *API {
})
})
})
r.Route("/templates/{template}", func(r chi.Router) {
r.Route("/templates", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractTemplateParam(options.Database),
)
r.Get("/daus", api.templateDAUs)
r.Get("/", api.template)
r.Delete("/", api.deleteTemplate)
r.Patch("/", api.patchTemplateMeta)
r.Route("/versions", func(r chi.Router) {
r.Post("/archive", api.postArchiveTemplateVersions)
r.Get("/", api.templateVersionsByTemplate)
r.Patch("/", api.patchActiveTemplateVersion)
r.Get("/{templateversionname}", api.templateVersionByName)
r.Get("/", api.fetchTemplates(nil))
r.Route("/{template}", func(r chi.Router) {
r.Use(
httpmw.ExtractTemplateParam(options.Database),
)
r.Get("/daus", api.templateDAUs)
r.Get("/", api.template)
r.Delete("/", api.deleteTemplate)
r.Patch("/", api.patchTemplateMeta)
r.Route("/versions", func(r chi.Router) {
r.Post("/archive", api.postArchiveTemplateVersions)
r.Get("/", api.templateVersionsByTemplate)
r.Patch("/", api.patchActiveTemplateVersion)
r.Get("/{templateversionname}", api.templateVersionByName)
})
})
})
r.Route("/templateversions/{templateversion}", func(r chi.Router) {

View File

@ -435,55 +435,78 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
// @Param organization path string true "Organization ID" format(uuid)
// @Success 200 {array} codersdk.Template
// @Router /organizations/{organization}/templates [get]
func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
func (api *API) templatesByOrganization() http.HandlerFunc {
// TODO: Should deprecate this endpoint and make it akin to /workspaces with
// a filter. There isn't a need to make the organization filter argument
// part of the query url.
// mutate the filter to only include templates from the given organization.
return api.fetchTemplates(func(r *http.Request, arg *database.GetTemplatesWithFilterParams) {
organization := httpmw.OrganizationParam(r)
arg.OrganizationID = organization.ID
})
}
p := httpapi.NewQueryParamParser()
values := r.URL.Query()
// @Summary Get all templates
// @ID get-all-templates
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Success 200 {array} codersdk.Template
// @Router /templates [get]
func (api *API) fetchTemplates(mutate func(r *http.Request, arg *database.GetTemplatesWithFilterParams)) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
deprecated := sql.NullBool{}
if values.Has("deprecated") {
deprecated = sql.NullBool{
Bool: p.Boolean(values, false, "deprecated"),
Valid: true,
p := httpapi.NewQueryParamParser()
values := r.URL.Query()
deprecated := sql.NullBool{}
if values.Has("deprecated") {
deprecated = sql.NullBool{
Bool: p.Boolean(values, false, "deprecated"),
Valid: true,
}
}
if len(p.Errors) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid query params.",
Validations: p.Errors,
})
return
}
}
if len(p.Errors) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid query params.",
Validations: p.Errors,
})
return
}
prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error preparing sql filter.",
Detail: err.Error(),
})
return
}
prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error preparing sql filter.",
Detail: err.Error(),
})
return
}
// Filter templates based on rbac permissions
templates, err := api.Database.GetAuthorizedTemplates(ctx, database.GetTemplatesWithFilterParams{
OrganizationID: organization.ID,
Deprecated: deprecated,
}, prepared)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
args := database.GetTemplatesWithFilterParams{
Deprecated: deprecated,
}
if mutate != nil {
mutate(r, &args)
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching templates in organization.",
Detail: err.Error(),
})
return
}
// Filter templates based on rbac permissions
templates, err := api.Database.GetAuthorizedTemplates(ctx, args, prepared)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates))
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching templates in organization.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates))
}
}
// @Summary Get templates by organization and template name

View File

@ -438,6 +438,42 @@ func TestTemplatesByOrganization(t *testing.T) {
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
require.NoError(t, err)
require.Len(t, templates, 2)
// Listing all should match
templates, err = client.Templates(ctx)
require.NoError(t, err)
require.Len(t, templates, 2)
})
t.Run("MultipleOrganizations", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
org2 := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
user, _ := coderdtest.CreateAnotherUser(t, client, org2.ID)
// 2 templates in first organization
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version2.ID)
// 2 in the second organization
version3 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil)
version4 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil)
coderdtest.CreateTemplate(t, client, org2.ID, version3.ID)
coderdtest.CreateTemplate(t, client, org2.ID, version4.ID)
ctx := testutil.Context(t, testutil.WaitLong)
// All 4 are viewable by the owner
templates, err := client.Templates(ctx)
require.NoError(t, err)
require.Len(t, templates, 4)
// Only 2 are viewable by the org user
templates, err = user.Templates(ctx)
require.NoError(t, err)
require.Len(t, templates, 2)
})
}