coderd/organizationskillpreferences.go
Rama main · réplica de lectura del repositorio original.
package coderd
import (
"bytes"
"context"
"fmt"
"net/http"
"slices"
"strings"
"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/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"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/codersdk"
)
// @Summary Get organization skill settings
// @ID get-organization-skill-settings
// @Security CoderSessionToken
// @Produce json
// @Tags Organizations
// @Param organization path string true "Organization ID or name"
// @Success 200 {object} codersdk.OrganizationSkillSettings
// @Router /api/experimental/organizations/{organization}/skill-settings [get]
// @x-apidocgen {"skip": true}
func (api *API) getOrganizationSkillSettings(rw http.ResponseWriter, r *http.Request) { //nolint:revive // Method name matches route.
organization := httpmw.OrganizationParam(r)
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.OrganizationSkillSettings{
MembersCanDisableSkills: organization.MembersCanDisableSkills,
})
}
// @Summary Update organization skill settings
// @ID update-organization-skill-settings
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Organizations
// @Param organization path string true "Organization ID or name"
// @Param request body codersdk.UpdateOrganizationSkillSettingsRequest true "Update organization skill settings request"
// @Success 200 {object} codersdk.OrganizationSkillSettings
// @Router /api/experimental/organizations/{organization}/skill-settings [put]
// @x-apidocgen {"skip": true}
func (api *API) putOrganizationSkillSettings(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
organization = httpmw.OrganizationParam(r)
auditor = api.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
OrganizationID: organization.ID,
})
)
defer commitAudit()
aReq.Old = organization
if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceOrganizationSkill.InOrg(organization.ID)) {
httpapi.Write(ctx, rw, http.StatusForbidden, organizationSkillForbidden("update organization skill settings"))
return
}
var req codersdk.UpdateOrganizationSkillSettingsRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
updated, err := api.Database.UpdateOrganizationMembersCanDisableSkills(ctx, database.UpdateOrganizationMembersCanDisableSkillsParams{
ID: organization.ID,
MembersCanDisableSkills: req.MembersCanDisableSkills,
UpdatedAt: dbtime.Now(),
})
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
aReq.New = updated
httpapi.Write(ctx, rw, http.StatusOK, codersdk.OrganizationSkillSettings{
MembersCanDisableSkills: updated.MembersCanDisableSkills,
})
}
// @Summary Get organization skill preferences for a member
// @ID get-organization-skill-preferences-for-a-member
// @Security CoderSessionToken
// @Produce json
// @Tags Organizations
// @Param organization path string true "Organization ID or name"
// @Param user path string true "User ID, name, or me"
// @Success 200 {object} codersdk.OrganizationSkillPreferences
// @Router /api/experimental/organizations/{organization}/members/{user}/skill-preferences [get]
// @x-apidocgen {"skip": true}
func (api *API) getOrganizationSkillPreferences(rw http.ResponseWriter, r *http.Request) { //nolint:revive // Method name matches route.
ctx := r.Context()
member := httpmw.OrganizationMemberParam(r)
prefs, err := api.Database.GetOrganizationSkillMemberPreferences(ctx, database.GetOrganizationSkillMemberPreferencesParams{
OrganizationID: member.OrganizationID,
UserID: member.UserID,
})
if err != nil {
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusOK, organizationSkillPreferences(prefs))
}
// @Summary Update organization skill preferences for a member
// @ID update-organization-skill-preferences-for-a-member
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Organizations
// @Param organization path string true "Organization ID or name"
// @Param user path string true "User ID, name, or me"
// @Param request body codersdk.UpdateOrganizationSkillPreferencesRequest true "Update organization skill preferences request"
// @Success 200 {object} codersdk.OrganizationSkillPreferences
// @Router /api/experimental/organizations/{organization}/members/{user}/skill-preferences [patch]
// @x-apidocgen {"skip": true}
func (api *API) patchOrganizationSkillPreferences(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
organization = httpmw.OrganizationParam(r)
member = httpmw.OrganizationMemberParam(r)
)
if !api.Authorize(r, policy.ActionUpdatePersonal, rbac.ResourceUserObject(member.UserID)) {
httpapi.ResourceNotFound(rw)
return
}
if !organization.MembersCanDisableSkills {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: "Members cannot turn off organization skills in this organization.",
Detail: "An organization admin must let members turn off organization skills first.",
})
return
}
var req codersdk.UpdateOrganizationSkillPreferencesRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
if req.AllSkillsDisabled == nil && len(req.Skills) == 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid request to update organization skill preferences.",
Detail: "Set all_skills_disabled or at least one entry in skills.",
})
return
}
skillIDs, validations, err := api.parseOrganizationSkillPreferenceIDs(ctx, organization.ID, member.UserID, req.Skills)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
if len(validations) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid request to update organization skill preferences.",
Validations: validations,
})
return
}
err = api.Database.InTx(func(tx database.Store) error {
if req.AllSkillsDisabled != nil {
err := tx.UpsertOrganizationMemberSkillSettings(ctx, database.UpsertOrganizationMemberSkillSettingsParams{
OrganizationID: member.OrganizationID,
UserID: member.UserID,
AllSkillsDisabled: *req.AllSkillsDisabled,
})
if err != nil {
return xerrors.Errorf("upsert organization member skill settings: %w", err)
}
}
for _, skill := range skillIDs {
if skill.enabled {
err := tx.DeleteOrganizationMemberDisabledSkill(ctx, database.DeleteOrganizationMemberDisabledSkillParams{
OrganizationID: member.OrganizationID,
UserID: member.UserID,
SkillID: skill.id,
})
if err != nil {
return xerrors.Errorf("delete organization member disabled skill: %w", err)
}
continue
}
err := tx.InsertOrganizationMemberDisabledSkill(ctx, database.InsertOrganizationMemberDisabledSkillParams{
OrganizationID: member.OrganizationID,
UserID: member.UserID,
SkillID: skill.id,
})
if err != nil {
return xerrors.Errorf("insert organization member disabled skill: %w", err)
}
}
return nil
}, nil)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
prefs, err := api.Database.GetOrganizationSkillMemberPreferences(ctx, database.GetOrganizationSkillMemberPreferencesParams{
OrganizationID: member.OrganizationID,
UserID: member.UserID,
})
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusOK, organizationSkillPreferences(prefs))
}
type organizationSkillPreferenceChange struct {
id uuid.UUID
enabled bool
}
// parseOrganizationSkillPreferenceIDs validates that every key names an
// organization skill of the organization that the member can read, and
// returns the changes sorted by skill ID.
func (api *API) parseOrganizationSkillPreferenceIDs(ctx context.Context, organizationID, memberID uuid.UUID, choices map[string]bool) ([]organizationSkillPreferenceChange, []codersdk.ValidationError, error) {
if len(choices) == 0 {
return nil, nil, nil
}
// Owners and user admins can edit another member's choices, so check
// the skills against what that member can read, as chatd does.
member, _, err := httpmw.UserRBACSubject(ctx, api.Database, memberID, rbac.ScopeAll)
if err != nil {
return nil, nil, xerrors.Errorf("load member authorization: %w", err)
}
//nolint:gocritic // Org skill ACLs are evaluated against the member.
rows, err := api.Database.ListOrganizationSkillMetadataByOrganizationID(dbauthz.As(ctx, member), organizationID)
if err != nil {
return nil, nil, xerrors.Errorf("list organization skill metadata: %w", err)
}
readable := make(map[uuid.UUID]struct{}, len(rows))
for _, row := range rows {
readable[row.ID] = struct{}{}
}
var (
keysByID = make(map[uuid.UUID][]string, len(choices))
validations []codersdk.ValidationError
)
for key := range choices {
field := "skills." + key
id, err := uuid.Parse(key)
if err != nil {
validations = append(validations, codersdk.ValidationError{
Field: field,
Detail: fmt.Sprintf("%q is not a valid skill ID.", key),
})
continue
}
if _, ok := readable[id]; !ok {
validations = append(validations, codersdk.ValidationError{
Field: field,
Detail: "No organization skill with this ID exists in this organization.",
})
continue
}
keysByID[id] = append(keysByID[id], key)
}
changes := make([]organizationSkillPreferenceChange, 0, len(keysByID))
for id, keys := range keysByID {
// uuid.Parse accepts several spellings of one ID, so two keys can
// name the same skill. Reject them instead of letting map order
// pick the stored value.
if len(keys) > 1 {
for _, key := range keys {
validations = append(validations, codersdk.ValidationError{
Field: "skills." + key,
Detail: "Another key in skills names the same skill.",
})
}
continue
}
changes = append(changes, organizationSkillPreferenceChange{id: id, enabled: choices[keys[0]]})
}
slices.SortFunc(validations, func(a, b codersdk.ValidationError) int {
return strings.Compare(a.Field, b.Field)
})
slices.SortFunc(changes, func(a, b organizationSkillPreferenceChange) int {
return bytes.Compare(a.id[:], b.id[:])
})
return changes, validations, nil
}
func organizationSkillPreferences(prefs database.GetOrganizationSkillMemberPreferencesRow) codersdk.OrganizationSkillPreferences {
disabled := prefs.DisabledSkillIDs
if disabled == nil {
disabled = []uuid.UUID{}
}
return codersdk.OrganizationSkillPreferences{
MembersCanDisableSkills: prefs.MembersCanDisableSkills,
AllSkillsDisabled: prefs.AllSkillsDisabled,
DisabledSkillIDs: disabled,
}
}