Skip to content

Commit

Permalink
Allow automatic creation of software install policy for VPP and FMA a…
Browse files Browse the repository at this point in the history
…pps in API (#26440)

For #26190. FMA is included here because the previous implementation was
client-side. QA'd manually. Follow-up PR soon with automated test coverage.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [ ] Added/updated automated tests
- [x] A detailed QA plan exists on the associated ticket (if it isn't
there, work with the product group's QA engineer to add it)
- [x] Manual QA for all new/changed functionality
  • Loading branch information
iansltx authored Feb 22, 2025
1 parent defe2dc commit ce36352
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 101 deletions.
1 change: 1 addition & 0 deletions changes/23744-vpp-automatic-install
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Allowed VPP apps to be automatically installed via a Fleet-created policy. Also added auto-install to FMA via the API, replacing a more brittle client-side implementation.
3 changes: 2 additions & 1 deletion ee/server/service/maintained_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (svc *Service) AddFleetMaintainedApp(
teamID *uint,
appID uint,
installScript, preInstallQuery, postInstallScript, uninstallScript string,
selfService bool,
selfService bool, automaticInstall bool,
labelsIncludeAny, labelsExcludeAny []string,
) (titleID uint, err error) {
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil {
Expand Down Expand Up @@ -137,6 +137,7 @@ func (svc *Service) AddFleetMaintainedApp(
InstallScript: installScript,
UninstallScript: uninstallScript,
ValidatedLabels: validatedLabels,
AutomaticInstall: automaticInstall,
}

// Create record in software installers table
Expand Down
13 changes: 12 additions & 1 deletion ee/server/service/vpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,14 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
if err := svc.authz.Authorize(ctx, &fleet.VPPApp{TeamID: teamID}, fleet.ActionWrite); err != nil {
return err
}
if appID.AddAutoInstallPolicy {
// Currently, same write permissions are applied on software and policies,
// but leaving this here in case it changes in the future.
if err := svc.authz.Authorize(ctx, &fleet.Policy{PolicyData: fleet.PolicyData{TeamID: teamID}}, fleet.ActionWrite); err != nil {
return err
}
}

// Validate platform
if appID.Platform == "" {
appID.Platform = fleet.MacOSPlatform
Expand Down Expand Up @@ -329,7 +337,10 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
}

if appID.SelfService && appID.Platform != fleet.MacOSPlatform {
return fleet.NewUserMessageError(errors.New("Currently, self-service only supports macOS"), http.StatusBadRequest)
return fleet.NewUserMessageError(errors.New("Currently, self-service is only supported on macOS, Windows, and Linux. Please add the app without self_service and manually install it on the Host details page."), http.StatusBadRequest)
}
if appID.AddAutoInstallPolicy && appID.Platform != fleet.MacOSPlatform {
return fleet.NewUserMessageError(errors.New("Currently, automatic install is only supported on macOS, Windows, and Linux. Please add the app without automatic_install and manually install it on the Host details page."), http.StatusBadRequest)
}

vppToken, err := svc.getVPPToken(ctx, teamID)
Expand Down
193 changes: 130 additions & 63 deletions pkg/automatic_policy/automatic_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,76 @@ type PolicyData struct {
}

// InstallerMetadata contains the metadata of a software package used to generate the policies.
type InstallerMetadata struct {
type InstallerMetadata interface {
PolicyName() (string, error)
PolicyDescription() (string, error)
PolicyQuery() (string, error)
PolicyPlatform() (string, error)
}

type MacInstallerMetadata struct {
BundleIdentifier string
// Title is the software title extracted from a software package.
Title string
}

func (m MacInstallerMetadata) PolicyName() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
return fmt.Sprintf("[Install software] %s", m.Title), nil
}

func (m MacInstallerMetadata) PolicyDescription() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
return fmt.Sprintf("Policy triggers automatic install of %s on each host that's missing this software.", m.Title), nil
}

func (m MacInstallerMetadata) PolicyQuery() (string, error) {
if m.BundleIdentifier == "" {
return "", ErrMissingBundleIdentifier
}
return fmt.Sprintf("SELECT 1 FROM apps WHERE bundle_identifier = '%s';", m.BundleIdentifier), nil
}

func (m MacInstallerMetadata) PolicyPlatform() (string, error) {
return "darwin", nil
}

type FullInstallerMetadata struct {
// BundleIdentifier is the bundle identifier for 'pkg' packages
BundleIdentifier string

// Title is the software title extracted from a software package.
Title string

// Extension is the extension of the software package.
Extension string
// BundleIdentifier contains the bundle identifier for 'pkg' packages.
BundleIdentifier string

// PackageIDs contains the product code for 'msi' packages.
PackageIDs []string
}

var (
// ErrExtensionNotSupported is returned if the extension is not supported to generate automatic policies.
ErrExtensionNotSupported = errors.New("extension not supported")
// ErrMissingBundleIdentifier is returned if the software extension is "pkg" and a bundle identifier was not extracted from the installer.
ErrMissingBundleIdentifier = errors.New("missing bundle identifier")
// ErrMissingProductCode is returned if the software extension is "msi" and a product code was not extracted from the installer.
ErrMissingProductCode = errors.New("missing product code")
// ErrMissingTitle is returned if a title was not extracted from the installer.
ErrMissingTitle = errors.New("missing title")
)

// Generate generates the "trigger policy" from the metadata of a software package.
func Generate(metadata InstallerMetadata) (*PolicyData, error) {
switch {
case metadata.Title == "":
return nil, ErrMissingTitle
case metadata.Extension != "pkg" && metadata.Extension != "msi" && metadata.Extension != "deb" && metadata.Extension != "rpm":
return nil, ErrExtensionNotSupported
case metadata.Extension == "pkg" && metadata.BundleIdentifier == "":
return nil, ErrMissingBundleIdentifier
case metadata.Extension == "msi" && (len(metadata.PackageIDs) == 0 || metadata.PackageIDs[0] == ""):
return nil, ErrMissingProductCode
func (m FullInstallerMetadata) PolicyName() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
if m.Extension == "" {
return "", ErrExtensionNotSupported
}
return fmt.Sprintf("[Install software] %s (%s)", m.Title, m.Extension), nil
}

name := fmt.Sprintf("[Install software] %s (%s)", metadata.Title, metadata.Extension)

description := fmt.Sprintf("Policy triggers automatic install of %s on each host that's missing this software.", metadata.Title)
if metadata.Extension == "deb" || metadata.Extension == "rpm" {
func (m FullInstallerMetadata) PolicyDescription() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
description := fmt.Sprintf("Policy triggers automatic install of %s on each host that's missing this software.", m.Title)
if m.Extension == "deb" || m.Extension == "rpm" {
basedPrefix := "RPM"
if metadata.Extension == "rpm" {
if m.Extension == "rpm" {
basedPrefix = "Debian"
}
description += fmt.Sprintf(
Expand All @@ -68,49 +97,87 @@ func Generate(metadata InstallerMetadata) (*PolicyData, error) {
)
}

switch metadata.Extension {
return description, nil
}

func (m FullInstallerMetadata) PolicyQuery() (string, error) {
switch m.Extension {
case "pkg":
return &PolicyData{
Name: name,
Query: fmt.Sprintf("SELECT 1 FROM apps WHERE bundle_identifier = '%s';", metadata.BundleIdentifier),
Platform: "darwin",
Description: description,
}, nil
if m.BundleIdentifier == "" {
return "", ErrMissingBundleIdentifier
}
return fmt.Sprintf("SELECT 1 FROM apps WHERE bundle_identifier = '%s';", m.BundleIdentifier), nil
case "msi":
return &PolicyData{
Name: name,
Query: fmt.Sprintf("SELECT 1 FROM programs WHERE identifying_number = '%s';", metadata.PackageIDs[0]),
Platform: "windows",
Description: description,
}, nil
if len(m.PackageIDs) == 0 || m.PackageIDs[0] == "" {
return "", ErrMissingProductCode
}
return fmt.Sprintf("SELECT 1 FROM programs WHERE identifying_number = '%s';", m.PackageIDs[0]), nil
case "deb":
return &PolicyData{
Name: name,
Query: fmt.Sprintf(
// First inner SELECT will mark the policies as successful on non-DEB-based hosts.
`SELECT 1 WHERE EXISTS (
return fmt.Sprintf(
// First inner SELECT will mark the policies as successful on non-DEB-based hosts.
`SELECT 1 WHERE EXISTS (
SELECT 1 WHERE (SELECT COUNT(*) FROM deb_packages) = 0
) OR EXISTS (
SELECT 1 FROM deb_packages WHERE name = '%s'
);`, metadata.Title,
),
Platform: "linux",
Description: description,
}, nil
);`, m.Title,
), nil
case "rpm":
return &PolicyData{
Name: name,
Query: fmt.Sprintf(
// First inner SELECT will mark the policies as successful on non-RPM-based hosts.
`SELECT 1 WHERE EXISTS (
return fmt.Sprintf(
// First inner SELECT will mark the policies as successful on non-RPM-based hosts.
`SELECT 1 WHERE EXISTS (
SELECT 1 WHERE (SELECT COUNT(*) FROM rpm_packages) = 0
) OR EXISTS (
SELECT 1 FROM rpm_packages WHERE name = '%s'
);`, metadata.Title),
Platform: "linux",
Description: description,
}, nil
);`, m.Title), nil
default:
return "", ErrExtensionNotSupported
}
}

func (m FullInstallerMetadata) PolicyPlatform() (string, error) {
switch m.Extension {
case "pkg":
return "darwin", nil
case "msi":
return "windows", nil
case "deb":
return "linux", nil
case "rpm":
return "linux", nil
default:
return nil, ErrExtensionNotSupported
return "", ErrExtensionNotSupported
}
}

var (
// ErrExtensionNotSupported is returned if the extension is not supported to generate automatic policies.
ErrExtensionNotSupported = errors.New("extension not supported")
// ErrMissingBundleIdentifier is returned if the software extension is "pkg" and a bundle identifier was not extracted from the installer.
ErrMissingBundleIdentifier = errors.New("missing bundle identifier")
// ErrMissingProductCode is returned if the software extension is "msi" and a product code was not extracted from the installer.
ErrMissingProductCode = errors.New("missing product code")
// ErrMissingTitle is returned if a title was not extracted from the installer.
ErrMissingTitle = errors.New("missing title")
)

// Generate generates the "trigger policy" from the metadata of a software package.
func Generate(metadata InstallerMetadata) (*PolicyData, error) {
name, err := metadata.PolicyName()
if err != nil {
return nil, err
}
query, err := metadata.PolicyQuery()
if err != nil {
return nil, err
}
platform, err := metadata.PolicyPlatform()
if err != nil {
return nil, err
}
description, err := metadata.PolicyDescription()
if err != nil {
return nil, err
}

return &PolicyData{Name: name, Query: query, Description: description, Platform: platform}, nil
}
18 changes: 9 additions & 9 deletions pkg/automatic_policy/automatic_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,38 @@ import (
)

func TestGenerateErrors(t *testing.T) {
_, err := Generate(InstallerMetadata{
_, err := Generate(FullInstallerMetadata{
Title: "Foobar",
Extension: "exe",
BundleIdentifier: "",
PackageIDs: []string{"Foobar"},
})
require.ErrorIs(t, err, ErrExtensionNotSupported)

_, err = Generate(InstallerMetadata{
_, err = Generate(FullInstallerMetadata{
Title: "Foobar",
Extension: "msi",
BundleIdentifier: "",
PackageIDs: []string{""},
})
require.ErrorIs(t, err, ErrMissingProductCode)
_, err = Generate(InstallerMetadata{
_, err = Generate(FullInstallerMetadata{
Title: "Foobar",
Extension: "msi",
BundleIdentifier: "",
PackageIDs: []string{},
})
require.ErrorIs(t, err, ErrMissingProductCode)

_, err = Generate(InstallerMetadata{
_, err = Generate(FullInstallerMetadata{
Title: "Foobar",
Extension: "pkg",
BundleIdentifier: "",
PackageIDs: []string{""},
})
require.ErrorIs(t, err, ErrMissingBundleIdentifier)

_, err = Generate(InstallerMetadata{
_, err = Generate(FullInstallerMetadata{
Title: "",
Extension: "deb",
BundleIdentifier: "",
Expand All @@ -48,7 +48,7 @@ func TestGenerateErrors(t *testing.T) {
}

func TestGenerate(t *testing.T) {
policyData, err := Generate(InstallerMetadata{
policyData, err := Generate(FullInstallerMetadata{
Title: "Foobar",
Extension: "pkg",
BundleIdentifier: "com.foo.bar",
Expand All @@ -60,7 +60,7 @@ func TestGenerate(t *testing.T) {
require.Equal(t, "darwin", policyData.Platform)
require.Equal(t, "SELECT 1 FROM apps WHERE bundle_identifier = 'com.foo.bar';", policyData.Query)

policyData, err = Generate(InstallerMetadata{
policyData, err = Generate(FullInstallerMetadata{
Title: "Barfoo",
Extension: "msi",
BundleIdentifier: "",
Expand All @@ -72,7 +72,7 @@ func TestGenerate(t *testing.T) {
require.Equal(t, "windows", policyData.Platform)
require.Equal(t, "SELECT 1 FROM programs WHERE identifying_number = 'foo';", policyData.Query)

policyData, err = Generate(InstallerMetadata{
policyData, err = Generate(FullInstallerMetadata{
Title: "Zoobar",
Extension: "deb",
BundleIdentifier: "",
Expand All @@ -89,7 +89,7 @@ Software won't be installed on Linux hosts with RPM-based distributions because
SELECT 1 FROM deb_packages WHERE name = 'Zoobar'
);`, policyData.Query)

policyData, err = Generate(InstallerMetadata{
policyData, err = Generate(FullInstallerMetadata{
Title: "Barzoo",
Extension: "rpm",
BundleIdentifier: "",
Expand Down
Loading

0 comments on commit ce36352

Please sign in to comment.