mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
chore: implement fetch all authorized templates api (#13678)
This commit is contained in:
28
coderd/apidoc/docs.go
generated
28
coderd/apidoc/docs.go
generated
@ -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": [
|
||||
|
24
coderd/apidoc/swagger.json
generated
24
coderd/apidoc/swagger.json
generated
@ -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": [
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user