Ctrl K
ComunidadGuías…

coderd/organizationskills.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)}
}