coderd/organizationskills.go
Rama main · réplica de lectura del repositorio original.
package coderd
import (
"fmt"
"net/http"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/x/skills"
"github.com/coder/coder/v2/codersdk"
)
const (
// skillsPerOrganizationLimitConstraint is raised by the skill cap trigger
// with USING CONSTRAINT, so dbgen does not emit it.
skillsPerOrganizationLimitConstraint database.CheckConstraint = "skills_per_organization_limit"
// maxSkillsPerOrganization matches the cap trigger, which applies one
// limit to every owner kind.
maxSkillsPerOrganization = skills.MaxPersonalSkillsPerUser
)
// @Summary Create an organization skill
// @ID create-an-organization-skill
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Organizations
// @Param organization path string true "Organization ID or name"
// @Param request body codersdk.CreateSkillRequest true "Create organization skill request"
// @Success 201 {object} codersdk.Skill
// @Router /api/experimental/organizations/{organization}/skills [post]
// @x-apidocgen {"skip": true}
func (api *API) postOrganizationSkill(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
organization = httpmw.OrganizationParam(r)
auditor = api.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationSkill](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
OrganizationID: organization.ID,
})
)
defer commitAudit()
if !api.Authorize(r, policy.ActionCreate, rbac.ResourceOrganizationSkill.InOrg(organization.ID)) {
httpapi.Write(ctx, rw, http.StatusForbidden, organizationSkillCreateErrors.forbidden)
return
}
skill, ok := createSkill(ctx, rw, r, organizationSkillCreateErrors, skillOwnerOrganization, func(parsed skills.ParsedSkill, create skillCreate) (database.Skill, error) {
return api.Database.InsertOrganizationSkill(ctx, database.InsertOrganizationSkillParams{
ID: uuid.New(),
OrganizationID: organization.ID,
Name: parsed.Name,
Description: parsed.Description,
Content: create.Content,
ModelInvocable: create.ModelInvocable,
// The Everyone group shares the organization's ID, so new skills
// reach every member until an admin narrows the ACL.
GroupACL: database.ChatACL{
organization.ID.String(): {Permissions: []policy.Action{policy.ActionRead}},
},
UserACL: database.ChatACL{},
})
})
if !ok {
return
}
aReq.New = database.AuditableOrganizationSkill{Skill: skill}
}
// @Summary List organization skills
// @ID list-organization-skills
// @Security CoderSessionToken
// @Produce json
// @Tags Organizations
// @Param organization path string true "Organization ID or name"
// @Success 200 {array} codersdk.SkillMetadata
// @Router /api/experimental/organizations/{organization}/skills [get]
// @x-apidocgen {"skip": true}
func (api *API) getOrganizationSkills(rw http.ResponseWriter, r *http.Request) { //nolint:revive // Method name matches route.
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
rows, err := api.Database.ListOrganizationSkillMetadataByOrganizationID(ctx, organization.ID)
if err != nil {
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OrganizationSkillMetadataList(rows))
}
// @Summary Get an organization skill by name
// @ID get-an-organization-skill-by-name
// @Security CoderSessionToken
// @Produce json
// @Tags Organizations
// @Param organization path string true "Organization ID or name"
// @Param skillName path string true "Skill name"
// @Success 200 {object} codersdk.Skill
// @Router /api/experimental/organizations/{organization}/skills/{skillName} [get]
// @x-apidocgen {"skip": true}
func (api *API) getOrganizationSkill(rw http.ResponseWriter, r *http.Request) { //nolint:revive // Method name matches route.
httpapi.Write(r.Context(), rw, http.StatusOK, db2sdk.Skill(httpmw.OrganizationSkillParam(r)))
}
// @Summary Update an organization skill
// @ID update-an-organization-skill
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Organizations
// @Param organization path string true "Organization ID or name"
// @Param skillName path string true "Skill name"
// @Param request body codersdk.UpdateSkillRequest true "Update organization skill request"
// @Success 200 {object} codersdk.Skill
// @Router /api/experimental/organizations/{organization}/skills/{skillName} [patch]
// @x-apidocgen {"skip": true}
func (api *API) patchOrganizationSkill(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
oldSkill = httpmw.OrganizationSkillParam(r)
auditor = api.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationSkill](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
OrganizationID: oldSkill.OrganizationID.UUID,
})
)
defer commitAudit()
aReq.Old = database.AuditableOrganizationSkill{Skill: oldSkill}
current, skill, ok := api.updateSkill(ctx, rw, r, oldSkill.Name, organizationSkillUpdateErrors, skillOwnerOrganization, func(tx database.Store, update skillUpdate) (database.Skill, database.Skill, error) {
//nolint:gocritic // The update below reauthorizes the locked row for update.
locked, err := tx.GetOrganizationSkillByIDForUpdate(dbauthz.AsSystemRestricted(ctx), oldSkill.ID)
if err != nil {
return database.Skill{}, database.Skill{}, xerrors.Errorf("lock organization skill: %w", err)
}
updated, err := tx.UpdateOrganizationSkillByOrganizationIDAndName(ctx, database.UpdateOrganizationSkillByOrganizationIDAndNameParams{
Description: update.Description,
Content: update.Content,
Enabled: update.Enabled,
ModelInvocable: update.ModelInvocable,
OrganizationID: locked.OrganizationID.UUID,
Name: locked.Name,
})
if err != nil {
return database.Skill{}, database.Skill{}, xerrors.Errorf("update organization skill: %w", err)
}
return locked, updated, nil
})
if !ok {
return
}
// The audit diff starts from the locked row, not the middleware's read.
aReq.Old = database.AuditableOrganizationSkill{Skill: current}
aReq.New = database.AuditableOrganizationSkill{Skill: skill}
}
// @Summary Delete an organization skill
// @ID delete-an-organization-skill
// @Security CoderSessionToken
// @Tags Organizations
// @Param organization path string true "Organization ID or name"
// @Param skillName path string true "Skill name"
// @Success 204
// @Router /api/experimental/organizations/{organization}/skills/{skillName} [delete]
// @x-apidocgen {"skip": true}
func (api *API) deleteOrganizationSkill(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
skill = httpmw.OrganizationSkillParam(r)
auditor = api.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationSkill](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionDelete,
OrganizationID: skill.OrganizationID.UUID,
})
)
defer commitAudit()
aReq.Old = database.AuditableOrganizationSkill{Skill: skill}
deleted, err := api.Database.DeleteOrganizationSkillByOrganizationIDAndName(ctx, database.DeleteOrganizationSkillByOrganizationIDAndNameParams{
OrganizationID: skill.OrganizationID.UUID,
Name: skill.Name,
})
if err != nil {
switch {
case httpapi.IsUnauthorizedError(err):
httpapi.Write(ctx, rw, http.StatusForbidden, organizationSkillForbidden("delete this organization skill"))
case httpapi.Is404Error(err):
httpapi.ResourceNotFound(rw)
default:
httpapi.InternalServerError(rw, err)
}
return
}
aReq.Old = database.AuditableOrganizationSkill{Skill: deleted}
rw.WriteHeader(http.StatusNoContent)
}
var (
// These replace httpapi.Forbidden, whose detail says the caller cannot
// view content these callers may be able to read.
organizationSkillCreateErrors = skillWriteErrors{
forbidden: organizationSkillForbidden("create organization skills"),
limit: skillsPerOrganizationLimitConstraint,
limitReached: codersdk.Response{
Message: "Organization skill limit reached.",
Detail: fmt.Sprintf("Each organization can have at most %d skills.", maxSkillsPerOrganization),
},
nameIndex: database.UniqueSkillsOrganizationIDNameIndex,
}
organizationSkillUpdateErrors = skillWriteErrors{
forbidden: organizationSkillForbidden("update this organization skill"),
}
)
func organizationSkillForbidden(action string) codersdk.Response {
return codersdk.Response{Message: fmt.Sprintf("You don't have permission to %s.", action)}
}