mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
#31977 Tested with: 224 scripts, 102 Apple configuration profiles, 105 Apple declarations, 37 Windows configuration profiles. Scattered in 7 teams. No variables on profiles/scripts: - scan scripts: 22.079416ms - scan apple profiles: 1.865083ms - scan apple declarations: 683.75µs - scan windows profiles: 531.167µs A variable "X" on with profiles/scripts that have variable "Y": - scan scripts: 26.305459ms - scan apple profiles: 1.841708ms - scan apple declarations: 756.917µs - scan windows profiles: 571.875µs --- ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually For unreleased bug fixes in a release candidate, one of: - [X] Confirmed that the fix is not expected to adversely impact load test results - [X] Alerted the release DRI if additional load testing is needed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Prevents deleting a secret that’s still in use. The system now checks scripts, Apple/Windows MDM profiles, and Apple declarations across teams before allowing deletion. - The delete dialog now shows a clear message indicating which item (and team) is using the secret, replacing the previous generic error. - Deletion succeeds once all references are removed, ensuring safer secret management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
256 lines
7.7 KiB
Go
256 lines
7.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/authz"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
)
|
|
|
|
const (
|
|
SecretVariablePrefix = "FLEET_SECRET_"
|
|
)
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
// Create secret variables (spec)
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type createSecretVariablesRequest struct {
|
|
DryRun bool `json:"dry_run"`
|
|
SecretVariables []fleet.SecretVariable `json:"secrets"`
|
|
}
|
|
|
|
type createSecretVariablesResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r createSecretVariablesResponse) Error() error { return r.Err }
|
|
|
|
func createSecretVariablesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*createSecretVariablesRequest)
|
|
err := svc.CreateSecretVariables(ctx, req.SecretVariables, req.DryRun)
|
|
return createSecretVariablesResponse{Err: err}, nil
|
|
}
|
|
|
|
func (svc *Service) CreateSecretVariables(ctx context.Context, secretVariables []fleet.SecretVariable, dryRun bool) error {
|
|
// Do authorization check first so that we don't have to worry about it later in the flow.
|
|
if err := svc.authz.Authorize(ctx, &fleet.SecretVariable{}, fleet.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
privateKey := svc.config.Server.PrivateKey
|
|
if testSetEmptyPrivateKey {
|
|
privateKey = ""
|
|
}
|
|
|
|
if len(privateKey) == 0 {
|
|
return ctxerr.Wrap(ctx,
|
|
&fleet.BadRequestError{Message: "Couldn't save secret variables. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key"})
|
|
}
|
|
|
|
// Preprocess: strip FLEET_SECRET_ prefix from variable names
|
|
for i, secretVariable := range secretVariables {
|
|
secretVariables[i].Name = fleet.Preprocess(strings.TrimPrefix(secretVariable.Name, SecretVariablePrefix))
|
|
}
|
|
|
|
for _, secretVariable := range secretVariables {
|
|
if err := fleet.ValidateSecretVariableName(secretVariable.Name); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "validate secret variable name")
|
|
}
|
|
}
|
|
|
|
if dryRun {
|
|
return nil
|
|
}
|
|
|
|
if err := svc.ds.UpsertSecretVariables(ctx, secretVariables); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "saving secret variables")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
// Create secret variable
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type createSecretVariableRequest struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type createSecretVariableResponse struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r createSecretVariableResponse) Error() error { return r.Err }
|
|
|
|
func createSecretVariableEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*createSecretVariableRequest)
|
|
id, err := svc.CreateSecretVariable(ctx, req.Name, req.Value)
|
|
if err != nil {
|
|
return createSecretVariableResponse{Err: err}, nil
|
|
}
|
|
return createSecretVariableResponse{
|
|
ID: id,
|
|
Name: req.Name,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) CreateSecretVariable(ctx context.Context, name string, value string) (id uint, err error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.SecretVariable{}, fleet.ActionWrite); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
privateKey := svc.config.Server.PrivateKey
|
|
if testSetEmptyPrivateKey {
|
|
privateKey = ""
|
|
}
|
|
|
|
if len(privateKey) == 0 {
|
|
return 0, ctxerr.Wrap(ctx,
|
|
&fleet.BadRequestError{
|
|
Message: "Couldn't save secret variable. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key",
|
|
})
|
|
}
|
|
|
|
if err := fleet.ValidateSecretVariableName(name); err != nil {
|
|
return 0, ctxerr.Wrap(ctx, err, "validate secret variable name")
|
|
}
|
|
if value == "" {
|
|
return 0, fleet.NewInvalidArgumentError("name", "secret variable value cannot be empty")
|
|
}
|
|
|
|
id, err = svc.ds.CreateSecretVariable(ctx, name, value)
|
|
if err != nil {
|
|
return 0, ctxerr.Wrap(ctx, err, "saving secret variable")
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
authz.UserFromContext(ctx),
|
|
fleet.ActivityCreatedCustomVariable{
|
|
CustomVariableID: id,
|
|
CustomVariableName: name,
|
|
},
|
|
); err != nil {
|
|
return 0, ctxerr.Wrap(ctx, err, "create activity for secret variable creation")
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
// List secret variables
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type listSecretVariablesRequest struct {
|
|
ListOptions fleet.ListOptions `url:"list_options"`
|
|
}
|
|
|
|
type listSecretVariablesResponse struct {
|
|
CustomVariables []fleet.SecretVariableIdentifier `json:"custom_variables"`
|
|
Meta *fleet.PaginationMetadata `json:"meta"`
|
|
Count int `json:"count"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r listSecretVariablesResponse) Error() error { return r.Err }
|
|
|
|
func listSecretVariablesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*listSecretVariablesRequest)
|
|
secretVariables, meta, count, err := svc.ListSecretVariables(ctx, req.ListOptions)
|
|
return listSecretVariablesResponse{
|
|
CustomVariables: secretVariables,
|
|
Meta: meta,
|
|
Count: count,
|
|
|
|
Err: err,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) ListSecretVariables(
|
|
ctx context.Context,
|
|
opts fleet.ListOptions,
|
|
) (
|
|
secretVariables []fleet.SecretVariableIdentifier,
|
|
meta *fleet.PaginationMetadata,
|
|
count int,
|
|
err error,
|
|
) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.SecretVariable{}, fleet.ActionRead); err != nil {
|
|
return nil, nil, 0, err
|
|
}
|
|
|
|
// MatchQuery/After currently not supported
|
|
opts.MatchQuery = ""
|
|
opts.After = ""
|
|
// Always include pagination info.
|
|
opts.IncludeMetadata = true
|
|
// Default sort order is name ascending.
|
|
opts.OrderKey = "name"
|
|
opts.OrderDirection = fleet.OrderAscending
|
|
|
|
secretVariables, meta, count, err = svc.ds.ListSecretVariables(ctx, opts)
|
|
if err != nil {
|
|
return nil, nil, 0, ctxerr.Wrap(ctx, err, "list secret variables")
|
|
}
|
|
|
|
return secretVariables, meta, count, nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
// Delete secret variable
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type deleteSecretVariableRequest struct {
|
|
ID uint `url:"id"`
|
|
}
|
|
|
|
type deleteSecretVariableResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r deleteSecretVariableResponse) Error() error { return r.Err }
|
|
|
|
func deleteSecretVariableEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*deleteSecretVariableRequest)
|
|
err := svc.DeleteSecretVariable(ctx, req.ID)
|
|
return deleteSecretVariableResponse{
|
|
Err: err,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) DeleteSecretVariable(ctx context.Context, id uint) error {
|
|
if err := svc.authz.Authorize(ctx, &fleet.SecretVariable{}, fleet.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
deletedSecretVariableName, err := svc.ds.DeleteSecretVariable(ctx, id)
|
|
if err != nil {
|
|
var secretUsedErr *fleet.SecretUsedError
|
|
if errors.As(err, &secretUsedErr) {
|
|
return ctxerr.Wrap(ctx, &fleet.ConflictError{
|
|
Message: fmt.Sprintf("Couldn't delete. %s", secretUsedErr.Error()),
|
|
}, "delete secret variable")
|
|
}
|
|
return ctxerr.Wrap(ctx, err, "delete secret variable")
|
|
}
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
authz.UserFromContext(ctx),
|
|
fleet.ActivityDeletedCustomVariable{
|
|
CustomVariableID: id,
|
|
CustomVariableName: deletedSecretVariableName,
|
|
},
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "create activity for secret variable deletion")
|
|
}
|
|
return nil
|
|
}
|