2018-05-04 16:53:21 +00:00
package service
import (
"bytes"
2021-08-24 12:50:03 +00:00
"context"
2018-05-04 16:53:21 +00:00
"encoding/json"
2021-11-22 14:13:26 +00:00
"errors"
2018-05-04 16:53:21 +00:00
"fmt"
2021-02-03 02:55:16 +00:00
"io"
2018-05-04 16:53:21 +00:00
"net/http"
2021-02-03 02:55:16 +00:00
"os"
2023-02-15 18:01:44 +00:00
"path/filepath"
2023-11-15 21:04:24 +00:00
"strings"
2021-02-03 02:55:16 +00:00
"time"
2018-05-04 16:53:21 +00:00
2024-03-28 17:39:27 +00:00
"golang.org/x/text/unicode/norm"
2024-08-01 18:36:40 +00:00
"gopkg.in/yaml.v2"
2024-03-28 17:39:27 +00:00
2023-04-25 13:36:01 +00:00
"github.com/fleetdm/fleet/v4/pkg/optjson"
2022-08-05 22:07:32 +00:00
"github.com/fleetdm/fleet/v4/pkg/spec"
2021-11-22 14:13:26 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
2021-08-26 13:28:53 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2023-11-15 21:04:24 +00:00
"github.com/fleetdm/fleet/v4/server/mdm"
2024-09-06 22:10:28 +00:00
"github.com/fleetdm/fleet/v4/server/ptr"
2023-06-07 17:29:36 +00:00
kithttp "github.com/go-kit/kit/transport/http"
2018-05-04 16:53:21 +00:00
)
2024-03-28 17:39:27 +00:00
const batchSize = 100
2022-06-01 23:05:05 +00:00
// Client is used to consume Fleet APIs from Go code
2018-05-04 16:53:21 +00:00
type Client struct {
2022-06-01 23:05:05 +00:00
* baseClient
2022-06-07 20:00:09 +00:00
addr string
token string
customHeaders map [ string ] string
2021-08-26 13:28:53 +00:00
2023-06-15 17:13:41 +00:00
outputWriter io . Writer
errWriter io . Writer
2018-05-04 16:53:21 +00:00
}
2021-02-03 02:55:16 +00:00
type ClientOption func ( * Client ) error
func NewClient ( addr string , insecureSkipVerify bool , rootCA , urlPrefix string , options ... ClientOption ) ( * Client , error ) {
// TODO #265 refactor all optional parameters to functional options
// API breaking change, needs a major version release
2023-04-27 11:44:39 +00:00
baseClient , err := newBaseClient ( addr , insecureSkipVerify , rootCA , urlPrefix , nil , fleet . CapabilityMap { } )
2022-06-01 23:05:05 +00:00
if err != nil {
return nil , err
2018-10-01 22:23:46 +00:00
}
2021-02-03 02:55:16 +00:00
client := & Client {
2022-06-01 23:05:05 +00:00
baseClient : baseClient ,
addr : addr ,
2021-02-03 02:55:16 +00:00
}
for _ , option := range options {
err := option ( client )
if err != nil {
return nil , err
}
}
return client , nil
}
func EnableClientDebug ( ) ClientOption {
return func ( c * Client ) error {
httpClient , ok := c . http . ( * http . Client )
if ! ok {
return errors . New ( "client is not *http.Client" )
}
httpClient . Transport = & logRoundTripper { roundtripper : httpClient . Transport }
return nil
}
2018-05-04 16:53:21 +00:00
}
2023-06-15 17:13:41 +00:00
func SetClientOutputWriter ( w io . Writer ) ClientOption {
2021-08-26 13:28:53 +00:00
return func ( c * Client ) error {
2023-06-15 17:13:41 +00:00
c . outputWriter = w
return nil
}
}
func SetClientErrorWriter ( w io . Writer ) ClientOption {
return func ( c * Client ) error {
c . errWriter = w
2021-08-26 13:28:53 +00:00
return nil
}
}
2022-06-07 20:00:09 +00:00
// WithCustomHeaders sets custom headers to be sent with every request made
// with the client.
func WithCustomHeaders ( headers map [ string ] string ) ClientOption {
return func ( c * Client ) error {
// clone the map to prevent any changes in the original affecting the client
m := make ( map [ string ] string , len ( headers ) )
for k , v := range headers {
m [ k ] = v
}
c . customHeaders = m
return nil
}
}
2022-10-05 22:53:54 +00:00
func ( c * Client ) doContextWithBodyAndHeaders ( ctx context . Context , verb , path , rawQuery string , bodyBytes [ ] byte , headers map [ string ] string ) ( * http . Response , error ) {
2021-08-24 12:50:03 +00:00
request , err := http . NewRequestWithContext (
ctx ,
2018-05-04 16:53:21 +00:00
verb ,
2020-11-05 04:45:16 +00:00
c . url ( path , rawQuery ) . String ( ) ,
2018-05-07 19:44:40 +00:00
bytes . NewBuffer ( bodyBytes ) ,
2018-05-04 16:53:21 +00:00
)
if err != nil {
2021-11-22 14:13:26 +00:00
return nil , ctxerr . Wrap ( ctx , err , "creating request object" )
2018-05-04 16:53:21 +00:00
}
2022-06-07 20:00:09 +00:00
// set the custom headers first, they should not override the actual headers
// we set explicitly.
for k , v := range c . customHeaders {
request . Header . Set ( k , v )
}
2018-05-04 16:53:21 +00:00
for k , v := range headers {
request . Header . Set ( k , v )
}
2021-08-26 13:28:53 +00:00
resp , err := c . http . Do ( request )
if err != nil {
2021-11-22 14:13:26 +00:00
return nil , ctxerr . Wrap ( ctx , err , "do request" )
2021-08-26 13:28:53 +00:00
}
if resp . Header . Get ( fleet . HeaderLicenseKey ) == fleet . HeaderLicenseValueExpired {
2023-06-15 17:13:41 +00:00
fleet . WriteExpiredLicenseBanner ( c . errWriter )
2021-08-26 13:28:53 +00:00
}
return resp , nil
2018-05-04 16:53:21 +00:00
}
2022-10-05 22:53:54 +00:00
func ( c * Client ) doContextWithHeaders ( ctx context . Context , verb , path , rawQuery string , params interface { } , headers map [ string ] string ) ( * http . Response , error ) {
var bodyBytes [ ] byte
var err error
if params != nil {
bodyBytes , err = json . Marshal ( params )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "marshaling json" )
}
}
return c . doContextWithBodyAndHeaders ( ctx , verb , path , rawQuery , bodyBytes , headers )
}
2020-11-05 04:45:16 +00:00
func ( c * Client ) Do ( verb , path , rawQuery string , params interface { } ) ( * http . Response , error ) {
2021-08-24 12:50:03 +00:00
return c . DoContext ( context . Background ( ) , verb , path , rawQuery , params )
}
func ( c * Client ) DoContext ( ctx context . Context , verb , path , rawQuery string , params interface { } ) ( * http . Response , error ) {
2018-05-04 16:53:21 +00:00
headers := map [ string ] string {
"Content-type" : "application/json" ,
"Accept" : "application/json" ,
}
2021-08-24 12:50:03 +00:00
return c . doContextWithHeaders ( ctx , verb , path , rawQuery , params , headers )
2018-05-04 16:53:21 +00:00
}
2020-11-05 04:45:16 +00:00
func ( c * Client ) AuthenticatedDo ( verb , path , rawQuery string , params interface { } ) ( * http . Response , error ) {
2018-05-04 16:53:21 +00:00
if c . token == "" {
return nil , errors . New ( "authentication token is empty" )
}
headers := map [ string ] string {
"Content-Type" : "application/json" ,
"Accept" : "application/json" ,
"Authorization" : fmt . Sprintf ( "Bearer %s" , c . token ) ,
}
2021-08-24 12:50:03 +00:00
return c . doContextWithHeaders ( context . Background ( ) , verb , path , rawQuery , params , headers )
2018-05-04 16:53:21 +00:00
}
2024-05-07 16:22:05 +00:00
func ( c * Client ) AuthenticatedDoCustomHeaders ( verb , path , rawQuery string , params interface { } , customHeaders map [ string ] string ) ( * http . Response , error ) {
if c . token == "" {
return nil , errors . New ( "authentication token is empty" )
}
headers := map [ string ] string {
"Content-Type" : "application/json" ,
"Accept" : "application/json" ,
"Authorization" : fmt . Sprintf ( "Bearer %s" , c . token ) ,
}
for key , value := range customHeaders {
headers [ key ] = value
}
return c . doContextWithHeaders ( context . Background ( ) , verb , path , rawQuery , params , headers )
}
2018-05-04 16:53:21 +00:00
func ( c * Client ) SetToken ( t string ) {
c . token = t
}
2021-02-03 02:55:16 +00:00
// http.RoundTripper that will log debug information about the request and
// response, including paths, timing, and body.
//
// Inspired by https://stackoverflow.com/a/39528716/491710 and
// github.com/motemen/go-loghttp
type logRoundTripper struct {
roundtripper http . RoundTripper
}
// RoundTrip implements http.RoundTripper
func ( l * logRoundTripper ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
// Log request
fmt . Fprintf ( os . Stderr , "%s %s\n" , req . Method , req . URL )
reqBody , err := req . GetBody ( )
if err != nil {
fmt . Fprintf ( os . Stderr , "GetBody error: %v\n" , err )
} else {
defer reqBody . Close ( )
if _ , err := io . Copy ( os . Stderr , reqBody ) ; err != nil {
fmt . Fprintf ( os . Stderr , "Copy body error: %v\n" , err )
}
}
fmt . Fprintf ( os . Stderr , "\n" )
// Perform request using underlying roundtripper
start := time . Now ( )
res , err := l . roundtripper . RoundTrip ( req )
if err != nil {
fmt . Fprintf ( os . Stderr , "RoundTrip error: %v" , err )
return nil , err
}
// Log response
2021-05-17 17:29:50 +00:00
took := time . Since ( start ) . Truncate ( time . Millisecond )
2021-02-03 02:55:16 +00:00
fmt . Fprintf ( os . Stderr , "%s %s %s (%s)\n" , res . Request . Method , res . Request . URL , res . Status , took )
resBody := & bytes . Buffer { }
resBodyReader := io . TeeReader ( res . Body , resBody )
if _ , err := io . Copy ( os . Stderr , resBodyReader ) ; err != nil {
fmt . Fprintf ( os . Stderr , "Read body error: %v" , err )
return nil , err
}
2021-09-15 19:27:53 +00:00
res . Body = io . NopCloser ( resBody )
2021-02-03 02:55:16 +00:00
return res , nil
}
2021-07-16 18:28:13 +00:00
2021-09-14 13:58:48 +00:00
func ( c * Client ) authenticatedRequestWithQuery ( params interface { } , verb string , path string , responseDest interface { } , query string ) error {
response , err := c . AuthenticatedDo ( verb , path , query , params )
2021-07-16 18:28:13 +00:00
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "%s %s: %w" , verb , path , err )
2021-07-16 18:28:13 +00:00
}
defer response . Body . Close ( )
2022-06-01 23:05:05 +00:00
return c . parseResponse ( verb , path , response , responseDest )
2021-07-16 18:28:13 +00:00
}
2021-09-14 13:58:48 +00:00
func ( c * Client ) authenticatedRequest ( params interface { } , verb string , path string , responseDest interface { } ) error {
return c . authenticatedRequestWithQuery ( params , verb , path , responseDest , "" )
}
2022-08-05 22:07:32 +00:00
2023-11-01 14:13:12 +00:00
func ( c * Client ) CheckAnyMDMEnabled ( ) error {
return c . runAppConfigChecks ( func ( ac * fleet . EnrichedAppConfig ) error {
if ! ac . MDM . EnabledAndConfigured && ! ac . MDM . WindowsEnabledAndConfigured {
return errors . New ( fleet . MDMNotConfiguredMessage )
}
return nil
} )
}
func ( c * Client ) CheckAppleMDMEnabled ( ) error {
2023-06-07 17:29:36 +00:00
return c . runAppConfigChecks ( func ( ac * fleet . EnrichedAppConfig ) error {
if ! ac . MDM . EnabledAndConfigured {
2023-11-01 14:13:12 +00:00
return errors . New ( fleet . AppleMDMNotConfiguredMessage )
2023-06-07 17:29:36 +00:00
}
return nil
} )
2023-04-07 20:31:02 +00:00
}
2023-05-02 19:03:10 +00:00
func ( c * Client ) CheckPremiumMDMEnabled ( ) error {
2023-06-07 17:29:36 +00:00
return c . runAppConfigChecks ( func ( ac * fleet . EnrichedAppConfig ) error {
if ac . License == nil || ! ac . License . IsPremium ( ) {
return errors . New ( "missing or invalid license" )
}
if ! ac . MDM . EnabledAndConfigured {
2023-11-01 14:13:12 +00:00
return errors . New ( fleet . AppleMDMNotConfiguredMessage )
2023-06-07 17:29:36 +00:00
}
return nil
} )
}
func ( c * Client ) runAppConfigChecks ( fn func ( ac * fleet . EnrichedAppConfig ) error ) error {
2023-05-02 19:03:10 +00:00
appCfg , err := c . GetAppConfig ( )
if err != nil {
2023-06-07 17:29:36 +00:00
var sce kithttp . StatusCoder
if errors . As ( err , & sce ) && sce . StatusCode ( ) == http . StatusForbidden {
// do not return an error, user may not have permission to read app
// config (e.g. gitops) and those appconfig checks are just convenience
// to avoid the round-trip with potentially large payload to the server.
// Those will still be validated with the actual API call.
return nil
}
2023-05-02 19:03:10 +00:00
return err
}
2023-06-07 17:29:36 +00:00
return fn ( appCfg )
2023-05-02 19:03:10 +00:00
}
2024-01-26 16:00:58 +00:00
// getProfilesContents takes file paths and creates a slice of profile payloads
// ready to batch-apply.
2024-06-14 17:48:00 +00:00
func getProfilesContents ( baseDir string , macProfiles [ ] fleet . MDMProfileSpec , windowsProfiles [ ] fleet . MDMProfileSpec , expandEnv bool ) ( [ ] fleet . MDMProfileBatchPayload , error ) {
// map to check for duplicate names across all profiles
extByName := make ( map [ string ] string , len ( macProfiles ) )
result := make ( [ ] fleet . MDMProfileBatchPayload , 0 , len ( macProfiles ) )
// iterate over the profiles for each platform
for platform , profiles := range map [ string ] [ ] fleet . MDMProfileSpec {
"macos" : macProfiles ,
"windows" : windowsProfiles ,
} {
for _ , profile := range profiles {
filePath := resolveApplyRelativePath ( baseDir , profile . Path )
fileContents , err := os . ReadFile ( filePath )
2024-05-28 16:44:43 +00:00
if err != nil {
2024-06-14 17:48:00 +00:00
return nil , fmt . Errorf ( "applying custom settings: %w" , err )
2024-05-28 16:44:43 +00:00
}
2024-06-14 17:48:00 +00:00
if expandEnv {
fileContents , err = spec . ExpandEnvBytes ( fileContents )
if err != nil {
return nil , fmt . Errorf ( "expanding environment on file %q: %w" , profile . Path , err )
}
2023-11-15 21:04:24 +00:00
}
2024-06-14 17:48:00 +00:00
ext := filepath . Ext ( filePath )
// by default, use the file name (for macOS mobileconfig profiles, we'll switch to
// their PayloadDisplayName when we parse the profile below)
name := strings . TrimSuffix ( filepath . Base ( filePath ) , ext )
// for validation errors, we want to include the platform and file name in the error message
prefixErrMsg := fmt . Sprintf ( "Couldn't edit %s_settings.custom_settings (%s%s)" , platform , name , ext )
// validate macOS profiles
if platform == "macos" {
switch ext {
case ".mobileconfig" , ".xml" : // allowing .xml for backwards compatibility
mc , err := fleet . NewMDMAppleConfigProfile ( fileContents , nil )
if err != nil {
errForMsg := errors . Unwrap ( err )
if errForMsg == nil {
errForMsg = err
}
return nil , fmt . Errorf ( "%s: %w" , prefixErrMsg , errForMsg )
}
name = strings . TrimSpace ( mc . Name )
case ".json" :
if mdm . GetRawProfilePlatform ( fileContents ) != "darwin" {
return nil , fmt . Errorf ( "%s: %s" , prefixErrMsg , "Declaration profiles should include valid JSON." )
}
default :
return nil , fmt . Errorf ( "%s: %s" , prefixErrMsg , "macOS configuration profiles must be .mobileconfig or .json files." )
}
}
// validate windows profiles
if platform == "windows" {
switch ext {
case ".xml" :
if mdm . GetRawProfilePlatform ( fileContents ) != "windows" {
return nil , fmt . Errorf ( "%s: %s" , prefixErrMsg , "Windows configuration profiles can only have <Replace> or <Add> top level elements" )
}
default :
return nil , fmt . Errorf ( "%s: %s" , prefixErrMsg , "Windows configuration profiles must be .xml files." )
}
}
// check for duplicate names across all profiles
if e , isDuplicate := extByName [ name ] ; isDuplicate {
return nil , errors . New ( fmtDuplicateNameErrMsg ( name , e , ext ) )
}
extByName [ name ] = ext
result = append ( result , fleet . MDMProfileBatchPayload {
2024-06-25 19:26:28 +00:00
Name : name ,
Contents : fileContents ,
Labels : profile . Labels ,
LabelsIncludeAll : profile . LabelsIncludeAll ,
2024-11-05 20:13:44 +00:00
LabelsIncludeAny : profile . LabelsIncludeAny ,
2024-06-25 19:26:28 +00:00
LabelsExcludeAny : profile . LabelsExcludeAny ,
2024-06-14 17:48:00 +00:00
} )
}
2024-01-26 16:00:58 +00:00
}
return result , nil
2023-11-15 21:04:24 +00:00
}
2024-10-23 18:51:02 +00:00
// fileContent is used to store the name of a file and its content.
type fileContent struct {
Filename string
Content [ ] byte
}
// TODO: as confirmed by Noah and Marko on Slack:
//
// > from Noah: "We want to support existing features w/ fleetctl apply for
// > backwards compatibility GitOps but we don’ t need to add new features."
//
// We should deprecate ApplyGroup and use it only for `fleetctl apply` (and
// its current minimal use in `preview`), and have a distinct implementation
// that is `gitops`-only, because both uses have subtle differences in
// behaviour that make it hard to reuse a single implementation (e.g. a missing
// key in gitops means "remove what is absent" while in apply it means "leave
// as-is").
//
// For now I'm just passing a "gitops" bool for a quick fix, but we should
// properly plan that separation and refactor so that gitops can be
// significantly cleaned up and simplified going forward.
2022-08-05 22:07:32 +00:00
// ApplyGroup applies the given spec group to Fleet.
2023-02-22 16:14:53 +00:00
func ( c * Client ) ApplyGroup (
ctx context . Context ,
2024-10-23 18:51:02 +00:00
viaGitOps bool ,
2023-02-22 16:14:53 +00:00
specs * spec . Group ,
baseDir string ,
logf func ( format string , args ... interface { } ) ,
2024-08-05 17:39:10 +00:00
appconfig * fleet . EnrichedAppConfig ,
2024-05-28 16:44:43 +00:00
opts fleet . ApplyClientSpecOptions ,
2024-10-10 11:12:24 +00:00
teamsSoftwareInstallers map [ string ] [ ] fleet . SoftwarePackageResponse ,
teamsScripts map [ string ] [ ] fleet . ScriptResponse ,
2024-10-04 01:03:40 +00:00
) ( map [ string ] uint , map [ string ] [ ] fleet . SoftwarePackageResponse , map [ string ] [ ] fleet . ScriptResponse , error ) {
2022-08-05 22:07:32 +00:00
logfn := func ( format string , args ... interface { } ) {
if logf != nil {
logf ( format , args ... )
}
}
2023-07-25 00:17:20 +00:00
// specs.Queries must be applied before specs.Packs because packs reference queries.
2022-08-05 22:07:32 +00:00
if len ( specs . Queries ) > 0 {
2022-09-19 17:53:44 +00:00
if opts . DryRun {
logfn ( "[!] ignoring queries, dry run mode only supported for 'config' and 'team' specs\n" )
} else {
if err := c . ApplyQueries ( specs . Queries ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying queries: %w" , err )
2022-09-19 17:53:44 +00:00
}
logfn ( "[+] applied %d queries\n" , len ( specs . Queries ) )
2022-08-05 22:07:32 +00:00
}
}
if len ( specs . Labels ) > 0 {
2022-09-19 17:53:44 +00:00
if opts . DryRun {
logfn ( "[!] ignoring labels, dry run mode only supported for 'config' and 'team' specs\n" )
} else {
if err := c . ApplyLabels ( specs . Labels ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying labels: %w" , err )
2022-09-19 17:53:44 +00:00
}
logfn ( "[+] applied %d labels\n" , len ( specs . Labels ) )
2022-08-05 22:07:32 +00:00
}
}
if len ( specs . Packs ) > 0 {
2022-09-19 17:53:44 +00:00
if opts . DryRun {
logfn ( "[!] ignoring packs, dry run mode only supported for 'config' and 'team' specs\n" )
} else {
if err := c . ApplyPacks ( specs . Packs ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying packs: %w" , err )
2022-09-19 17:53:44 +00:00
}
logfn ( "[+] applied %d packs\n" , len ( specs . Packs ) )
2022-08-05 22:07:32 +00:00
}
}
if specs . AppConfig != nil {
2023-11-29 14:32:42 +00:00
windowsCustomSettings := extractAppCfgWindowsCustomSettings ( specs . AppConfig )
2023-11-15 21:04:24 +00:00
macosCustomSettings := extractAppCfgMacOSCustomSettings ( specs . AppConfig )
2024-01-26 16:00:58 +00:00
// if there is no custom setting but the windows and mac settings are
// non-nil, this means that we want to clear the existing custom settings,
// so we still go on with calling the batch-apply endpoint.
//
// TODO(mna): shouldn't that be an || instead of && ? I.e. if there are no
// custom settings but windows is present and empty (but mac is absent),
// shouldn't that clear the windows ones?
2024-06-14 17:48:00 +00:00
if ( windowsCustomSettings != nil && macosCustomSettings != nil ) || len ( windowsCustomSettings ) + len ( macosCustomSettings ) > 0 {
fileContents , err := getProfilesContents ( baseDir , macosCustomSettings , windowsCustomSettings , opts . ExpandEnvConfigProfiles )
2023-11-15 21:04:24 +00:00
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , err
2024-02-09 19:34:57 +00:00
}
// Figure out if MDM should be enabled.
assumeEnabled := false
// This cast is safe because we've already checked AppConfig when extracting custom settings
mdmConfigMap , ok := specs . AppConfig . ( map [ string ] interface { } ) [ "mdm" ] . ( map [ string ] interface { } )
if ok {
mdmEnabled , ok := mdmConfigMap [ "windows_enabled_and_configured" ]
if ok {
assumeEnabled , ok = mdmEnabled . ( bool )
assumeEnabled = ok && assumeEnabled
}
2023-02-15 18:01:44 +00:00
}
2024-05-28 16:44:43 +00:00
if err := c . ApplyNoTeamProfiles ( fileContents , opts . ApplySpecOptions , assumeEnabled ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying custom settings: %w" , err )
2023-02-15 18:01:44 +00:00
}
}
2023-04-07 20:31:02 +00:00
if macosSetup := extractAppCfgMacOSSetup ( specs . AppConfig ) ; macosSetup != nil {
2023-04-26 21:09:21 +00:00
if macosSetup . BootstrapPackage . Value != "" {
pkg , err := c . ValidateBootstrapPackageFromURL ( macosSetup . BootstrapPackage . Value )
2023-04-07 20:31:02 +00:00
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying fleet config: %w" , err )
2023-04-07 20:31:02 +00:00
}
if ! opts . DryRun {
if err := c . EnsureBootstrapPackage ( pkg , uint ( 0 ) ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying fleet config: %w" , err )
2023-04-25 13:36:01 +00:00
}
}
}
if macosSetup . MacOSSetupAssistant . Value != "" {
content , err := c . validateMacOSSetupAssistant ( resolveApplyRelativePath ( baseDir , macosSetup . MacOSSetupAssistant . Value ) )
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying fleet config: %w" , err )
2023-04-25 13:36:01 +00:00
}
if ! opts . DryRun {
if err := c . uploadMacOSSetupAssistant ( content , nil , macosSetup . MacOSSetupAssistant . Value ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying fleet config: %w" , err )
2023-04-07 20:31:02 +00:00
}
}
}
}
2023-10-10 22:00:45 +00:00
if scripts := extractAppCfgScripts ( specs . AppConfig ) ; scripts != nil {
2024-10-09 22:57:08 +00:00
scriptPayloads := make ( [ ] fleet . ScriptPayload , len ( scripts ) )
for i , f := range scripts {
2023-10-10 22:00:45 +00:00
b , err := os . ReadFile ( f )
if err != nil {
2024-11-13 17:01:08 +00:00
return nil , nil , nil , fmt . Errorf ( "applying no-team scripts: %w" , err )
2023-10-10 22:00:45 +00:00
}
scriptPayloads [ i ] = fleet . ScriptPayload {
ScriptContents : b ,
Name : filepath . Base ( f ) ,
}
}
2024-10-04 01:03:40 +00:00
noTeamScripts , err := c . ApplyNoTeamScripts ( scriptPayloads , opts . ApplySpecOptions )
if err != nil {
return nil , nil , nil , fmt . Errorf ( "applying no-team scripts: %w" , err )
2023-10-10 22:00:45 +00:00
}
2024-10-10 11:12:24 +00:00
teamsScripts [ "No team" ] = noTeamScripts
2023-10-10 22:00:45 +00:00
}
2024-11-13 17:01:08 +00:00
rules , err := extractAppCfgYaraRules ( specs . AppConfig )
if err != nil {
return nil , nil , nil , fmt . Errorf ( "applying yara rules: %w" , err )
}
if rules != nil {
rulePayloads := make ( [ ] fleet . YaraRule , len ( rules ) )
for i , f := range rules {
path := resolveApplyRelativePath ( baseDir , f . Path )
b , err := os . ReadFile ( path )
if err != nil {
return nil , nil , nil , fmt . Errorf ( "applying yara rules: %w" , err )
}
rulePayloads [ i ] = fleet . YaraRule {
Contents : string ( b ) ,
Name : filepath . Base ( f . Path ) ,
}
}
specs . AppConfig . ( map [ string ] interface { } ) [ "yara_rules" ] = rulePayloads
}
2024-05-28 16:44:43 +00:00
if err := c . ApplyAppConfig ( specs . AppConfig , opts . ApplySpecOptions ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying fleet config: %w" , err )
2022-08-05 22:07:32 +00:00
}
2022-09-19 17:53:44 +00:00
if opts . DryRun {
logfn ( "[+] would've applied fleet config\n" )
} else {
logfn ( "[+] applied fleet config\n" )
}
2022-08-05 22:07:32 +00:00
}
if specs . EnrollSecret != nil {
2024-05-31 12:01:13 +00:00
if err := c . ApplyEnrollSecretSpec ( specs . EnrollSecret , opts . ApplySpecOptions ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying enroll secrets: %w" , err )
2024-05-31 12:01:13 +00:00
}
2022-09-19 17:53:44 +00:00
if opts . DryRun {
2024-02-09 19:34:57 +00:00
logfn ( "[+] would've applied enroll secrets\n" )
2022-09-19 17:53:44 +00:00
} else {
logfn ( "[+] applied enroll secrets\n" )
2022-08-05 22:07:32 +00:00
}
}
2024-02-09 19:34:57 +00:00
var teamIDsByName map [ string ] uint
2022-08-05 22:07:32 +00:00
if len ( specs . Teams ) > 0 {
2023-02-15 18:01:44 +00:00
// extract the teams' custom settings and resolve the files immediately, so
// that any non-existing file error is found before applying the specs.
2023-11-15 21:04:24 +00:00
tmMDMSettings := extractTmSpecsMDMCustomSettings ( specs . Teams )
2023-02-15 18:01:44 +00:00
2024-01-26 16:00:58 +00:00
tmFileContents := make ( map [ string ] [ ] fleet . MDMProfileBatchPayload , len ( tmMDMSettings ) )
2024-06-14 17:48:00 +00:00
for k , profileSpecs := range tmMDMSettings {
fileContents , err := getProfilesContents ( baseDir , profileSpecs . macos , profileSpecs . windows , opts . ExpandEnvConfigProfiles )
2023-11-15 21:04:24 +00:00
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "Team %s: %w" , k , err ) // TODO: consider adding team name to improve error messages generally for other parts of the config because multiple team configs can be processed at once
2023-02-15 18:01:44 +00:00
}
tmFileContents [ k ] = fileContents
}
2023-04-07 20:31:02 +00:00
tmMacSetup := extractTmSpecsMacOSSetup ( specs . Teams )
tmBootstrapPackages := make ( map [ string ] * fleet . MDMAppleBootstrapPackage , len ( tmMacSetup ) )
2023-04-25 13:36:01 +00:00
tmMacSetupAssistants := make ( map [ string ] [ ] byte , len ( tmMacSetup ) )
2024-10-23 18:51:02 +00:00
// those are gitops-only features
tmMacSetupScript := make ( map [ string ] fileContent , len ( tmMacSetup ) )
tmMacSetupSoftware := make ( map [ string ] [ ] * fleet . MacOSSetupSoftware , len ( tmMacSetup ) )
// this is a set of software packages or VPP apps that are configured as
// install_during_setup, by team. This is a gitops-only setting, so it will
// only be filled when called via this command.
tmSoftwareMacOSSetup := make ( map [ string ] map [ fleet . MacOSSetupSoftware ] struct { } , len ( tmMacSetup ) )
2023-04-07 20:31:02 +00:00
for k , setup := range tmMacSetup {
2023-04-26 21:09:21 +00:00
if setup . BootstrapPackage . Value != "" {
bp , err := c . ValidateBootstrapPackageFromURL ( setup . BootstrapPackage . Value )
2023-04-07 20:31:02 +00:00
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying teams: %w" , err )
2023-04-07 20:31:02 +00:00
}
tmBootstrapPackages [ k ] = bp
}
2023-04-25 13:36:01 +00:00
if setup . MacOSSetupAssistant . Value != "" {
b , err := c . validateMacOSSetupAssistant ( resolveApplyRelativePath ( baseDir , setup . MacOSSetupAssistant . Value ) )
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying teams: %w" , err )
2023-04-25 13:36:01 +00:00
}
tmMacSetupAssistants [ k ] = b
}
2024-10-23 18:51:02 +00:00
if setup . Script . Value != "" {
b , err := c . validateMacOSSetupScript ( resolveApplyRelativePath ( baseDir , setup . Script . Value ) )
if err != nil {
return nil , nil , nil , fmt . Errorf ( "applying teams: %w" , err )
}
tmMacSetupScript [ k ] = fileContent { Filename : filepath . Base ( setup . Script . Value ) , Content : b }
}
if viaGitOps {
m , err := extractTeamOrNoTeamMacOSSetupSoftware ( baseDir , setup . Software . Value )
if err != nil {
return nil , nil , nil , err
}
tmSoftwareMacOSSetup [ k ] = m
tmMacSetupSoftware [ k ] = setup . Software . Value
}
2023-04-07 20:31:02 +00:00
}
2023-10-10 22:00:45 +00:00
tmScripts := extractTmSpecsScripts ( specs . Teams )
tmScriptsPayloads := make ( map [ string ] [ ] fleet . ScriptPayload , len ( tmScripts ) )
for k , paths := range tmScripts {
2024-10-09 22:57:08 +00:00
scriptPayloads := make ( [ ] fleet . ScriptPayload , len ( paths ) )
for i , f := range paths {
2023-10-10 22:00:45 +00:00
b , err := os . ReadFile ( f )
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying fleet config: %w" , err )
2023-10-10 22:00:45 +00:00
}
scriptPayloads [ i ] = fleet . ScriptPayload {
ScriptContents : b ,
Name : filepath . Base ( f ) ,
}
}
tmScriptsPayloads [ k ] = scriptPayloads
}
2024-07-22 17:19:19 +00:00
tmSoftwarePackages := extractTmSpecsSoftwarePackages ( specs . Teams )
2024-09-06 22:10:28 +00:00
tmSoftwarePackagesPayloads := make ( map [ string ] [ ] fleet . SoftwareInstallerPayload , len ( tmSoftwarePackages ) )
2024-10-23 18:51:02 +00:00
tmSoftwarePackageByPath := make ( map [ string ] map [ string ] fleet . SoftwarePackageSpec , len ( tmSoftwarePackages ) )
2024-07-10 18:53:03 +00:00
for tmName , software := range tmSoftwarePackages {
2024-10-23 18:51:02 +00:00
installDuringSetupKeys := tmSoftwareMacOSSetup [ tmName ]
2024-11-07 17:22:08 +00:00
softwarePayloads , err := buildSoftwarePackagesPayload ( software , installDuringSetupKeys )
2024-08-05 17:39:10 +00:00
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying software installers for team %q: %w" , tmName , err )
2024-05-14 18:06:33 +00:00
}
2024-07-22 17:19:19 +00:00
tmSoftwarePackagesPayloads [ tmName ] = softwarePayloads
2024-10-23 18:51:02 +00:00
for _ , swSpec := range software {
if swSpec . ReferencedYamlPath != "" {
// can be referenced by macos_setup.software.package_path
if tmSoftwarePackageByPath [ tmName ] == nil {
tmSoftwarePackageByPath [ tmName ] = make ( map [ string ] fleet . SoftwarePackageSpec , len ( software ) )
}
tmSoftwarePackageByPath [ tmName ] [ swSpec . ReferencedYamlPath ] = swSpec
}
}
2024-07-22 17:19:19 +00:00
}
tmSoftwareApps := extractTmSpecsSoftwareApps ( specs . Teams )
tmSoftwareAppsPayloads := make ( map [ string ] [ ] fleet . VPPBatchPayload )
2024-10-23 18:51:02 +00:00
tmSoftwareAppsByAppID := make ( map [ string ] map [ string ] fleet . TeamSpecAppStoreApp , len ( tmSoftwareApps ) )
2024-07-22 17:19:19 +00:00
for tmName , apps := range tmSoftwareApps {
2024-10-23 18:51:02 +00:00
installDuringSetupKeys := tmSoftwareMacOSSetup [ tmName ]
2024-07-22 17:19:19 +00:00
appPayloads := make ( [ ] fleet . VPPBatchPayload , 0 , len ( apps ) )
for _ , app := range apps {
2024-10-23 18:51:02 +00:00
var installDuringSetup * bool
if installDuringSetupKeys != nil {
_ , ok := installDuringSetupKeys [ fleet . MacOSSetupSoftware { AppStoreID : app . AppStoreID } ]
installDuringSetup = & ok
}
appPayloads = append ( appPayloads , fleet . VPPBatchPayload {
AppStoreID : app . AppStoreID ,
SelfService : app . SelfService ,
InstallDuringSetup : installDuringSetup ,
} )
// can be referenced by macos_setup.software.app_store_id
if tmSoftwareAppsByAppID [ tmName ] == nil {
tmSoftwareAppsByAppID [ tmName ] = make ( map [ string ] fleet . TeamSpecAppStoreApp , len ( apps ) )
}
tmSoftwareAppsByAppID [ tmName ] [ app . AppStoreID ] = app
2024-07-22 17:19:19 +00:00
}
tmSoftwareAppsPayloads [ tmName ] = appPayloads
2024-05-14 18:06:33 +00:00
}
2024-10-23 18:51:02 +00:00
// if macos_setup.software has some values, they must exist in the software
// packages or vpp apps.
for tmName , setupSw := range tmMacSetupSoftware {
if err := validateTeamOrNoTeamMacOSSetupSoftware ( tmName , setupSw , tmSoftwarePackageByPath [ tmName ] , tmSoftwareAppsByAppID [ tmName ] ) ; err != nil {
return nil , nil , nil , err
}
}
2023-02-15 18:01:44 +00:00
// Next, apply the teams specs before saving the profiles, so that any
// non-existing team gets created.
2024-02-09 19:34:57 +00:00
var err error
2024-05-03 13:03:00 +00:00
teamOpts := fleet . ApplyTeamSpecOptions {
2024-05-28 16:44:43 +00:00
ApplySpecOptions : opts . ApplySpecOptions ,
2024-05-03 13:03:00 +00:00
DryRunAssumptions : specs . TeamsDryRunAssumptions ,
}
2024-06-27 21:10:49 +00:00
// In dry-run, the team names returned are the old team names (when team name is modified via gitops)
2024-05-03 13:03:00 +00:00
teamIDsByName , err = c . ApplyTeams ( specs . Teams , teamOpts )
2023-06-07 17:29:36 +00:00
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying teams: %w" , err )
2022-08-05 22:07:32 +00:00
}
2023-02-15 18:01:44 +00:00
2024-06-27 21:10:49 +00:00
// When using GitOps, the team name could change, so we need to check for that
getTeamName := func ( teamName string ) string {
return teamName
}
if len ( specs . Teams ) == 1 && len ( teamIDsByName ) == 1 {
for key := range teamIDsByName {
if key != extractTeamName ( specs . Teams [ 0 ] ) {
getTeamName = func ( teamName string ) string {
return key
}
}
}
}
2023-02-15 18:01:44 +00:00
if len ( tmFileContents ) > 0 {
for tmName , profs := range tmFileContents {
2024-06-27 21:10:49 +00:00
// For non-dry run, currentTeamName and tmName are the same
currentTeamName := getTeamName ( tmName )
teamID , ok := teamIDsByName [ currentTeamName ]
2024-02-09 19:34:57 +00:00
if opts . DryRun && ( teamID == 0 || ! ok ) {
logfn ( "[+] would've applied MDM profiles for new team %s\n" , tmName )
} else {
logfn ( "[+] applying MDM profiles for team %s\n" , tmName )
2024-06-27 21:10:49 +00:00
if err := c . ApplyTeamProfiles ( currentTeamName , profs , teamOpts ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying custom settings for team %q: %w" , tmName , err )
2024-02-09 19:34:57 +00:00
}
2023-02-15 18:01:44 +00:00
}
}
}
2023-04-25 13:36:01 +00:00
if len ( tmBootstrapPackages ) + len ( tmMacSetupAssistants ) > 0 && ! opts . DryRun {
2023-06-07 17:29:36 +00:00
for tmName , tmID := range teamIDsByName {
if bp , ok := tmBootstrapPackages [ tmName ] ; ok {
if err := c . EnsureBootstrapPackage ( bp , tmID ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "uploading bootstrap package for team %q: %w" , tmName , err )
2023-04-07 20:31:02 +00:00
}
}
2023-06-07 17:29:36 +00:00
if b , ok := tmMacSetupAssistants [ tmName ] ; ok {
if err := c . uploadMacOSSetupAssistant ( b , & tmID , tmMacSetup [ tmName ] . MacOSSetupAssistant . Value ) ; err != nil {
2024-09-10 22:44:58 +00:00
if strings . Contains ( err . Error ( ) , "Couldn't upload" ) {
// Then the error should look something like this:
// "Couldn't upload. CONFIG_NAME_INVALID"
// We want the part after the period (this is the error name from Apple)
// to render a more helpful error message.
parts := strings . Split ( err . Error ( ) , "." )
if len ( parts ) < 2 {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "unexpected error while uploading macOS setup assistant for team %q: %w" , tmName , err )
2024-09-10 22:44:58 +00:00
}
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "Couldn't edit macos_setup_assistant. Response from Apple: %s. Learn more at %s" , strings . Trim ( parts [ 1 ] , " " ) , "https://fleetdm.com/learn-more-about/dep-profile" )
2024-09-10 22:44:58 +00:00
}
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "uploading macOS setup assistant for team %q: %w" , tmName , err )
2023-04-25 13:36:01 +00:00
}
}
2023-04-07 20:31:02 +00:00
}
}
2024-10-23 18:51:02 +00:00
if viaGitOps && ! opts . DryRun {
for tmName , tmID := range teamIDsByName {
if fc , ok := tmMacSetupScript [ tmName ] ; ok {
if err := c . uploadMacOSSetupScript ( fc . Filename , fc . Content , & tmID ) ; err != nil {
return nil , nil , nil , fmt . Errorf ( "uploading setup experience script for team %q: %w" , tmName , err )
}
} else {
if err := c . deleteMacOSSetupScript ( & tmID ) ; err != nil {
return nil , nil , nil , fmt . Errorf ( "deleting setup experience script for team %q: %w" , tmName , err )
}
}
}
}
2023-10-10 22:00:45 +00:00
if len ( tmScriptsPayloads ) > 0 {
for tmName , scripts := range tmScriptsPayloads {
2024-06-27 21:10:49 +00:00
// For non-dry run, currentTeamName and tmName are the same
currentTeamName := getTeamName ( tmName )
2024-10-04 01:03:40 +00:00
scriptResponses , err := c . ApplyTeamScripts ( currentTeamName , scripts , opts . ApplySpecOptions )
if err != nil {
return nil , nil , nil , fmt . Errorf ( "applying scripts for team %q: %w" , tmName , err )
2023-10-10 22:00:45 +00:00
}
2024-10-10 11:12:24 +00:00
teamsScripts [ tmName ] = scriptResponses
2023-10-10 22:00:45 +00:00
}
}
2024-07-22 17:19:19 +00:00
if len ( tmSoftwarePackagesPayloads ) > 0 {
for tmName , software := range tmSoftwarePackagesPayloads {
2024-06-27 21:10:49 +00:00
// For non-dry run, currentTeamName and tmName are the same
currentTeamName := getTeamName ( tmName )
2024-09-12 17:23:25 +00:00
logfn ( "[+] applying %d software packages for team %s\n" , len ( software ) , tmName )
2024-09-06 22:10:28 +00:00
installers , err := c . ApplyTeamSoftwareInstallers ( currentTeamName , software , opts . ApplySpecOptions )
if err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying software installers for team %q: %w" , tmName , err )
2024-05-14 18:06:33 +00:00
}
2024-10-10 11:12:24 +00:00
teamsSoftwareInstallers [ tmName ] = installers
2024-05-14 18:06:33 +00:00
}
}
2024-07-22 17:19:19 +00:00
if len ( tmSoftwareAppsPayloads ) > 0 {
for tmName , apps := range tmSoftwareAppsPayloads {
// For non-dry run, currentTeamName and tmName are the same
currentTeamName := getTeamName ( tmName )
2024-10-23 18:51:02 +00:00
logfn ( "[+] applying %d app store apps for team %s\n" , len ( apps ) , tmName )
2024-07-22 17:19:19 +00:00
if err := c . ApplyTeamAppStoreAppsAssociation ( currentTeamName , apps , opts . ApplySpecOptions ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying app store apps for team: %q: %w" , tmName , err )
2024-07-22 17:19:19 +00:00
}
}
}
2022-09-19 17:53:44 +00:00
if opts . DryRun {
logfn ( "[+] would've applied %d teams\n" , len ( specs . Teams ) )
} else {
logfn ( "[+] applied %d teams\n" , len ( specs . Teams ) )
}
2022-08-05 22:07:32 +00:00
}
2024-09-06 22:10:28 +00:00
// Policies can reference software installers thus they are applied at this point.
if len ( specs . Policies ) > 0 {
// Policy names must be unique, return error if duplicate policy names are found
if policyName := fleet . FirstDuplicatePolicySpecName ( specs . Policies ) ; policyName != "" {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf (
2024-09-06 22:10:28 +00:00
"applying policies: policy names must be unique. Please correct policy %q and try again." , policyName ,
)
}
if opts . DryRun {
logfn ( "[!] ignoring policies, dry run mode only supported for 'config' and 'team' specs\n" )
} else {
// If set, override the team in all the policies.
if opts . TeamForPolicies != "" {
for _ , policySpec := range specs . Policies {
policySpec . Team = opts . TeamForPolicies
}
}
if err := c . ApplyPolicies ( specs . Policies ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying policies: %w" , err )
2024-09-06 22:10:28 +00:00
}
logfn ( "[+] applied %d policies\n" , len ( specs . Policies ) )
}
}
2022-08-05 22:07:32 +00:00
if specs . UsersRoles != nil {
2022-09-19 17:53:44 +00:00
if opts . DryRun {
logfn ( "[!] ignoring user roles, dry run mode only supported for 'config' and 'team' specs\n" )
} else {
if err := c . ApplyUsersRoleSecretSpec ( specs . UsersRoles ) ; err != nil {
2024-10-04 01:03:40 +00:00
return nil , nil , nil , fmt . Errorf ( "applying user roles: %w" , err )
2022-09-19 17:53:44 +00:00
}
logfn ( "[+] applied user roles\n" )
2022-08-05 22:07:32 +00:00
}
}
2024-09-06 22:10:28 +00:00
2024-10-10 11:12:24 +00:00
return teamIDsByName , teamsSoftwareInstallers , teamsScripts , nil
2022-08-05 22:07:32 +00:00
}
2023-02-15 18:01:44 +00:00
2024-10-23 18:51:02 +00:00
func extractTeamOrNoTeamMacOSSetupSoftware ( baseDir string , software [ ] * fleet . MacOSSetupSoftware ) ( map [ fleet . MacOSSetupSoftware ] struct { } , error ) {
m := make ( map [ fleet . MacOSSetupSoftware ] struct { } , len ( software ) )
for _ , sw := range software {
if sw . AppStoreID != "" && sw . PackagePath != "" {
return nil , errors . New ( "applying teams: only one of app_store_id or package_path can be set" )
}
if sw . PackagePath != "" {
sw . PackagePath = resolveApplyRelativePath ( baseDir , sw . PackagePath )
}
m [ * sw ] = struct { } { }
}
return m , nil
}
func validateTeamOrNoTeamMacOSSetupSoftware ( teamName string , macOSSetupSoftware [ ] * fleet . MacOSSetupSoftware , packagesByPath map [ string ] fleet . SoftwarePackageSpec , vppAppsByAppID map [ string ] fleet . TeamSpecAppStoreApp ) error {
// if macos_setup.software has some values, they must exist in the software
// packages or vpp apps.
for _ , ssw := range macOSSetupSoftware {
var valid bool
if ssw . AppStoreID != "" {
// check that it exists in the team's Apps
_ , valid = vppAppsByAppID [ ssw . AppStoreID ]
} else if ssw . PackagePath != "" {
// check that it exists in the team's Software installers (PackagePath is
// already resolved to abs dir)
_ , valid = packagesByPath [ ssw . PackagePath ]
}
if ! valid {
label := ssw . AppStoreID
if label == "" {
label = ssw . PackagePath
}
return fmt . Errorf ( "applying macOS setup experience software for team %q: software %q does not exist for that team" , teamName , label )
}
}
return nil
}
2024-11-07 17:22:08 +00:00
func buildSoftwarePackagesPayload ( specs [ ] fleet . SoftwarePackageSpec , installDuringSetupKeys map [ fleet . MacOSSetupSoftware ] struct { } ) ( [ ] fleet . SoftwareInstallerPayload , error ) {
2024-08-05 17:39:10 +00:00
softwarePayloads := make ( [ ] fleet . SoftwareInstallerPayload , len ( specs ) )
for i , si := range specs {
var qc string
var err error
if si . PreInstallQuery . Path != "" {
2024-11-07 17:22:08 +00:00
queryFile := si . PreInstallQuery . Path
2024-08-05 17:39:10 +00:00
rawSpec , err := os . ReadFile ( queryFile )
if err != nil {
return nil , fmt . Errorf ( "reading pre-install query: %w" , err )
}
rawSpecExpanded , err := spec . ExpandEnvBytes ( rawSpec )
if err != nil {
return nil , fmt . Errorf ( "Couldn't exit software (%s). Unable to expand environment variable in YAML file %s: %w" , si . URL , queryFile , err )
}
var top any
if err := yaml . Unmarshal ( rawSpecExpanded , & top ) ; err != nil {
return nil , fmt . Errorf ( "Couldn't exit software (%s). Unable to expand environment variable in YAML file %s: %w" , si . URL , queryFile , err )
}
if _ , ok := top . ( map [ any ] any ) ; ok {
// Old apply format
group , err := spec . GroupFromBytes ( rawSpecExpanded )
if err != nil {
return nil , fmt . Errorf ( "Couldn't edit software (%s). Unable to parse pre-install apply format query YAML file %s: %w" , si . URL , queryFile , err )
}
if len ( group . Queries ) > 1 {
return nil , fmt . Errorf ( "Couldn't edit software (%s). Pre-install query YAML file %s should have only one query." , si . URL , queryFile )
}
if len ( group . Queries ) == 0 {
return nil , fmt . Errorf ( "Couldn't edit software (%s). Pre-install query YAML file %s doesn't have a query defined." , si . URL , queryFile )
}
qc = group . Queries [ 0 ] . Query
} else {
// Gitops format
var querySpecs [ ] fleet . QuerySpec
if err := yaml . Unmarshal ( rawSpecExpanded , & querySpecs ) ; err != nil {
return nil , fmt . Errorf ( "Couldn't edit software (%s). Unable to parse pre-install query YAML file %s: %w" , si . URL , queryFile , err )
}
if len ( querySpecs ) > 1 {
return nil , fmt . Errorf ( "Couldn't edit software (%s). Pre-install query YAML file %s should have only one query." , si . URL , queryFile )
}
if len ( querySpecs ) == 0 {
return nil , fmt . Errorf ( "Couldn't edit software (%s). Pre-install query YAML file %s doesn't have a query defined." , si . URL , queryFile )
}
qc = querySpecs [ 0 ] . Query
}
}
var ic [ ] byte
if si . InstallScript . Path != "" {
2024-11-07 17:22:08 +00:00
installScriptFile := si . InstallScript . Path
2024-08-05 17:39:10 +00:00
ic , err = os . ReadFile ( installScriptFile )
if err != nil {
return nil , fmt . Errorf ( "Couldn't edit software (%s). Unable to read install script file %s: %w" , si . URL , si . InstallScript . Path , err )
}
}
var pc [ ] byte
if si . PostInstallScript . Path != "" {
2024-11-07 17:22:08 +00:00
postInstallScriptFile := si . PostInstallScript . Path
2024-08-05 17:39:10 +00:00
pc , err = os . ReadFile ( postInstallScriptFile )
if err != nil {
return nil , fmt . Errorf ( "Couldn't edit software (%s). Unable to read post-install script file %s: %w" , si . URL , si . PostInstallScript . Path , err )
}
}
2024-09-12 18:25:40 +00:00
var us [ ] byte
if si . UninstallScript . Path != "" {
2024-11-07 17:22:08 +00:00
uninstallScriptFile := si . UninstallScript . Path
2024-09-12 18:25:40 +00:00
us , err = os . ReadFile ( uninstallScriptFile )
if err != nil {
return nil , fmt . Errorf ( "Couldn't edit software (%s). Unable to read uninstall script file %s: %w" , si . URL ,
si . UninstallScript . Path , err )
}
}
2024-10-23 18:51:02 +00:00
var installDuringSetup * bool
if installDuringSetupKeys != nil {
_ , ok := installDuringSetupKeys [ fleet . MacOSSetupSoftware { PackagePath : si . ReferencedYamlPath } ]
installDuringSetup = & ok
}
2024-08-05 17:39:10 +00:00
softwarePayloads [ i ] = fleet . SoftwareInstallerPayload {
2024-10-23 18:51:02 +00:00
URL : si . URL ,
SelfService : si . SelfService ,
PreInstallQuery : qc ,
InstallScript : string ( ic ) ,
PostInstallScript : string ( pc ) ,
UninstallScript : string ( us ) ,
InstallDuringSetup : installDuringSetup ,
2024-08-05 17:39:10 +00:00
}
}
return softwarePayloads , nil
}
2023-04-07 20:31:02 +00:00
func extractAppCfgMacOSSetup ( appCfg any ) * fleet . MacOSSetup {
asMap , ok := appCfg . ( map [ string ] interface { } )
if ! ok {
return nil
}
mmdm , ok := asMap [ "mdm" ] . ( map [ string ] interface { } )
if ! ok {
return nil
}
mos , ok := mmdm [ "macos_setup" ] . ( map [ string ] interface { } )
if ! ok || mos == nil {
return nil
}
2023-04-25 13:36:01 +00:00
bp , _ := mos [ "bootstrap_package" ] . ( string ) // if not a string, bp == ""
msa , _ := mos [ "macos_setup_assistant" ] . ( string )
2023-04-07 20:31:02 +00:00
return & fleet . MacOSSetup {
2023-04-26 21:09:21 +00:00
BootstrapPackage : optjson . SetString ( bp ) ,
2023-04-25 13:36:01 +00:00
MacOSSetupAssistant : optjson . SetString ( msa ) ,
2023-04-07 20:31:02 +00:00
}
}
2023-04-25 13:36:01 +00:00
func resolveApplyRelativePath ( baseDir , path string ) string {
return resolveApplyRelativePaths ( baseDir , [ ] string { path } ) [ 0 ]
}
// resolves the paths to an absolute path relative to the baseDir, which should
// be the path of the YAML file where the relative paths were specified. If the
// path is already absolute, it is left untouched.
func resolveApplyRelativePaths ( baseDir string , paths [ ] string ) [ ] string {
2023-02-15 18:01:44 +00:00
if baseDir == "" {
return paths
}
resolved := make ( [ ] string , 0 , len ( paths ) )
for _ , p := range paths {
if ! filepath . IsAbs ( p ) {
p = filepath . Join ( baseDir , p )
}
resolved = append ( resolved , p )
}
return resolved
}
2024-01-26 16:00:58 +00:00
func extractAppCfgCustomSettings ( appCfg interface { } , platformKey string ) [ ] fleet . MDMProfileSpec {
2023-02-15 18:01:44 +00:00
asMap , ok := appCfg . ( map [ string ] interface { } )
if ! ok {
return nil
}
2023-02-28 20:34:46 +00:00
mmdm , ok := asMap [ "mdm" ] . ( map [ string ] interface { } )
if ! ok {
return nil
}
2024-01-26 16:00:58 +00:00
mos , ok := mmdm [ platformKey ] . ( map [ string ] interface { } )
2023-02-15 18:01:44 +00:00
if ! ok || mos == nil {
return nil
}
cs , ok := mos [ "custom_settings" ]
if ! ok {
// custom settings is not present
return nil
}
csAny , ok := cs . ( [ ] interface { } )
if ! ok || csAny == nil {
// return a non-nil, empty slice instead, so the caller knows that the
// custom_settings key was actually provided.
2024-01-26 16:00:58 +00:00
return [ ] fleet . MDMProfileSpec { }
2023-02-15 18:01:44 +00:00
}
2024-06-25 19:26:28 +00:00
extractLabelField := func ( parentMap map [ string ] interface { } , fieldName string ) [ ] string {
var ret [ ] string
if labels , ok := parentMap [ fieldName ] . ( [ ] interface { } ) ; ok {
for _ , label := range labels {
if strLabel , ok := label . ( string ) ; ok {
ret = append ( ret , strLabel )
}
}
}
return ret
}
2024-01-26 16:00:58 +00:00
csSpecs := make ( [ ] fleet . MDMProfileSpec , 0 , len ( csAny ) )
2023-02-15 18:01:44 +00:00
for _ , v := range csAny {
2024-01-26 16:00:58 +00:00
if m , ok := v . ( map [ string ] interface { } ) ; ok {
var profSpec fleet . MDMProfileSpec
2023-02-15 18:01:44 +00:00
2024-01-26 16:00:58 +00:00
// extract the Path field
if path , ok := m [ "path" ] . ( string ) ; ok {
profSpec . Path = path
}
2023-11-29 14:32:42 +00:00
2024-06-25 19:26:28 +00:00
// at this stage we extract and return all supported label fields, the
// validations are done later on in the Fleet API endpoint.
profSpec . Labels = extractLabelField ( m , "labels" )
profSpec . LabelsIncludeAll = extractLabelField ( m , "labels_include_all" )
2024-11-05 20:13:44 +00:00
profSpec . LabelsIncludeAny = extractLabelField ( m , "labels_include_any" )
2024-06-25 19:26:28 +00:00
profSpec . LabelsExcludeAny = extractLabelField ( m , "labels_exclude_any" )
2023-11-29 14:32:42 +00:00
2024-01-26 16:00:58 +00:00
if profSpec . Path != "" {
csSpecs = append ( csSpecs , profSpec )
}
} else if m , ok := v . ( string ) ; ok { // for backwards compatibility with the old way to define profiles
if m != "" {
csSpecs = append ( csSpecs , fleet . MDMProfileSpec { Path : m } )
}
2023-11-29 14:32:42 +00:00
}
}
2024-01-26 16:00:58 +00:00
return csSpecs
}
func extractAppCfgMacOSCustomSettings ( appCfg interface { } ) [ ] fleet . MDMProfileSpec {
return extractAppCfgCustomSettings ( appCfg , "macos_settings" )
}
func extractAppCfgWindowsCustomSettings ( appCfg interface { } ) [ ] fleet . MDMProfileSpec {
return extractAppCfgCustomSettings ( appCfg , "windows_settings" )
2023-11-29 14:32:42 +00:00
}
2023-11-15 21:04:24 +00:00
2023-10-10 22:00:45 +00:00
func extractAppCfgScripts ( appCfg interface { } ) [ ] string {
asMap , ok := appCfg . ( map [ string ] interface { } )
if ! ok {
return nil
}
scripts , ok := asMap [ "scripts" ]
if ! ok {
// scripts is not present
return nil
}
scriptsAny , ok := scripts . ( [ ] interface { } )
if ! ok || scriptsAny == nil {
// return a non-nil, empty slice instead, so the caller knows that the
// scripts key was actually provided.
return [ ] string { }
}
scriptsStrings := make ( [ ] string , 0 , len ( scriptsAny ) )
for _ , v := range scriptsAny {
s , _ := v . ( string )
if s != "" {
scriptsStrings = append ( scriptsStrings , s )
}
}
return scriptsStrings
}
2024-11-13 17:01:08 +00:00
func extractAppCfgYaraRules ( appCfg interface { } ) ( [ ] fleet . YaraRuleSpec , error ) {
asMap , ok := appCfg . ( map [ string ] interface { } )
if ! ok {
return nil , errors . New ( "extract yara rules: app config is not a map" )
}
rules , ok := asMap [ "yara_rules" ]
if ! ok {
// yara_rules is not present. Return an empty slice so that the value is cleared.
return [ ] fleet . YaraRuleSpec { } , nil
}
rulesAny , ok := rules . ( [ ] interface { } )
if ! ok || rulesAny == nil {
// If nil, return an empty slice so the value will be cleared.
return [ ] fleet . YaraRuleSpec { } , nil
}
ruleSpecs := make ( [ ] fleet . YaraRuleSpec , 0 , len ( rulesAny ) )
for _ , v := range rulesAny {
smap , ok := v . ( map [ string ] interface { } )
if ! ok {
return nil , errors . New ( "extract yara rules: rule entry is not a map" )
}
pathEntry , ok := smap [ "path" ]
if ! ok {
return nil , errors . New ( "extract yara rules: rule entry missing path" )
}
path , ok := pathEntry . ( string )
if ! ok {
return nil , errors . New ( "extract yara rules: rule entry path is not string" )
}
ruleSpecs = append ( ruleSpecs , fleet . YaraRuleSpec { Path : path } )
}
return ruleSpecs , nil
}
2024-06-14 17:48:00 +00:00
type profileSpecsByPlatform struct {
macos [ ] fleet . MDMProfileSpec
windows [ ] fleet . MDMProfileSpec
}
2024-06-27 21:10:49 +00:00
func extractTeamName ( tmSpec json . RawMessage ) string {
var s struct {
Name string ` json:"name" `
}
if err := json . Unmarshal ( tmSpec , & s ) ; err != nil {
return ""
}
return norm . NFC . String ( s . Name )
}
2023-11-15 21:04:24 +00:00
// returns the custom macOS and Windows settings keyed by team name.
2024-06-14 17:48:00 +00:00
func extractTmSpecsMDMCustomSettings ( tmSpecs [ ] json . RawMessage ) map [ string ] profileSpecsByPlatform {
var m map [ string ] profileSpecsByPlatform
2023-02-15 18:01:44 +00:00
for _ , tm := range tmSpecs {
var spec struct {
2023-02-28 20:34:46 +00:00
Name string ` json:"name" `
MDM struct {
MacOSSettings struct {
CustomSettings json . RawMessage ` json:"custom_settings" `
} ` json:"macos_settings" `
2023-11-15 21:04:24 +00:00
WindowsSettings struct {
2023-11-29 14:32:42 +00:00
CustomSettings json . RawMessage ` json:"custom_settings" `
2023-11-15 21:04:24 +00:00
} ` json:"windows_settings" `
2023-02-28 20:34:46 +00:00
} ` json:"mdm" `
2023-02-15 18:01:44 +00:00
}
if err := json . Unmarshal ( tm , & spec ) ; err != nil {
// ignore, this will fail in the call to apply team specs
continue
}
2024-02-27 18:55:05 +00:00
spec . Name = norm . NFC . String ( spec . Name )
2023-11-15 21:04:24 +00:00
if spec . Name != "" {
2024-01-26 16:00:58 +00:00
var macOSSettings [ ] fleet . MDMProfileSpec
var windowsSettings [ ] fleet . MDMProfileSpec
2023-11-15 21:04:24 +00:00
// to keep existing bahavior, if any of the custom
// settings is provided, make the map a non-nil map
if len ( spec . MDM . MacOSSettings . CustomSettings ) > 0 ||
len ( spec . MDM . WindowsSettings . CustomSettings ) > 0 {
if m == nil {
2024-06-14 17:48:00 +00:00
m = make ( map [ string ] profileSpecsByPlatform )
2023-11-15 21:04:24 +00:00
}
}
if len ( spec . MDM . MacOSSettings . CustomSettings ) > 0 {
if err := json . Unmarshal ( spec . MDM . MacOSSettings . CustomSettings , & macOSSettings ) ; err != nil {
// ignore, will fail in apply team specs call
continue
}
if macOSSettings == nil {
// to be consistent with the AppConfig custom settings, set it to an
// empty slice if the provided custom settings are present but empty.
2024-01-26 16:00:58 +00:00
macOSSettings = [ ] fleet . MDMProfileSpec { }
2023-11-15 21:04:24 +00:00
}
2023-02-15 18:01:44 +00:00
}
2023-11-15 21:04:24 +00:00
if len ( spec . MDM . WindowsSettings . CustomSettings ) > 0 {
if err := json . Unmarshal ( spec . MDM . WindowsSettings . CustomSettings , & windowsSettings ) ; err != nil {
// ignore, will fail in apply team specs call
continue
}
if windowsSettings == nil {
// to be consistent with the AppConfig custom settings, set it to an
// empty slice if the provided custom settings are present but empty.
2024-01-26 16:00:58 +00:00
windowsSettings = [ ] fleet . MDMProfileSpec { }
2023-11-15 21:04:24 +00:00
}
2023-02-15 18:01:44 +00:00
}
2023-11-15 21:04:24 +00:00
2024-01-26 16:00:58 +00:00
// TODO: validate equal names here and API?
2024-06-14 17:48:00 +00:00
var result profileSpecsByPlatform
if macOSSettings != nil {
result . macos = macOSSettings
}
if windowsSettings != nil {
result . windows = windowsSettings
}
2023-11-15 21:04:24 +00:00
if macOSSettings != nil || windowsSettings != nil {
2024-06-14 17:48:00 +00:00
m [ spec . Name ] = result
2023-02-15 18:01:44 +00:00
}
}
}
return m
}
2023-04-07 20:31:02 +00:00
2024-08-05 17:39:10 +00:00
func extractTmSpecsSoftwarePackages ( tmSpecs [ ] json . RawMessage ) map [ string ] [ ] fleet . SoftwarePackageSpec {
var m map [ string ] [ ] fleet . SoftwarePackageSpec
2024-05-14 18:06:33 +00:00
for _ , tm := range tmSpecs {
var spec struct {
Name string ` json:"name" `
Software json . RawMessage ` json:"software" `
}
if err := json . Unmarshal ( tm , & spec ) ; err != nil {
// ignore, this will fail in the call to apply team specs
continue
}
spec . Name = norm . NFC . String ( spec . Name )
if spec . Name != "" && len ( spec . Software ) > 0 {
if m == nil {
2024-08-05 17:39:10 +00:00
m = make ( map [ string ] [ ] fleet . SoftwarePackageSpec )
2024-05-14 18:06:33 +00:00
}
2024-08-05 17:39:10 +00:00
var software fleet . SoftwareSpec
var packages [ ] fleet . SoftwarePackageSpec
2024-05-14 18:06:33 +00:00
if err := json . Unmarshal ( spec . Software , & software ) ; err != nil {
// ignore, will fail in apply team specs call
continue
}
2024-07-10 18:53:03 +00:00
if ! software . Packages . Valid {
2024-05-14 18:06:33 +00:00
// to be consistent with the AppConfig custom settings, set it to an
// empty slice if the provided custom settings are present but empty.
2024-08-05 17:39:10 +00:00
packages = [ ] fleet . SoftwarePackageSpec { }
2024-07-10 18:53:03 +00:00
} else {
packages = software . Packages . Value
2024-05-14 18:06:33 +00:00
}
2024-07-10 18:53:03 +00:00
m [ spec . Name ] = packages
2024-05-14 18:06:33 +00:00
}
}
return m
}
2024-07-22 17:19:19 +00:00
func extractTmSpecsSoftwareApps ( tmSpecs [ ] json . RawMessage ) map [ string ] [ ] fleet . TeamSpecAppStoreApp {
var m map [ string ] [ ] fleet . TeamSpecAppStoreApp
for _ , tm := range tmSpecs {
var spec struct {
Name string ` json:"name" `
Software json . RawMessage ` json:"software" `
}
if err := json . Unmarshal ( tm , & spec ) ; err != nil {
// ignore, this will fail in the call to apply team specs
continue
}
spec . Name = norm . NFC . String ( spec . Name )
if spec . Name != "" && len ( spec . Software ) > 0 {
if m == nil {
m = make ( map [ string ] [ ] fleet . TeamSpecAppStoreApp )
}
2024-08-05 17:39:10 +00:00
var software fleet . SoftwareSpec
2024-07-22 17:19:19 +00:00
var apps [ ] fleet . TeamSpecAppStoreApp
if err := json . Unmarshal ( spec . Software , & software ) ; err != nil {
// ignore, will fail in apply team specs call
continue
}
if ! software . AppStoreApps . Valid {
// to be consistent with the AppConfig custom settings, set it to an
// empty slice if the provided custom settings are present but empty.
apps = [ ] fleet . TeamSpecAppStoreApp { }
} else {
apps = software . AppStoreApps . Value
}
m [ spec . Name ] = apps
}
}
return m
}
2023-10-10 22:00:45 +00:00
func extractTmSpecsScripts ( tmSpecs [ ] json . RawMessage ) map [ string ] [ ] string {
var m map [ string ] [ ] string
for _ , tm := range tmSpecs {
var spec struct {
Name string ` json:"name" `
Scripts json . RawMessage ` json:"scripts" `
}
if err := json . Unmarshal ( tm , & spec ) ; err != nil {
// ignore, this will fail in the call to apply team specs
continue
}
2024-02-27 18:55:05 +00:00
spec . Name = norm . NFC . String ( spec . Name )
2023-10-10 22:00:45 +00:00
if spec . Name != "" && len ( spec . Scripts ) > 0 {
if m == nil {
m = make ( map [ string ] [ ] string )
}
var scripts [ ] string
if err := json . Unmarshal ( spec . Scripts , & scripts ) ; err != nil {
// ignore, will fail in apply team specs call
continue
}
if scripts == nil {
// to be consistent with the AppConfig custom settings, set it to an
// empty slice if the provided custom settings are present but empty.
scripts = [ ] string { }
}
m [ spec . Name ] = scripts
}
}
return m
}
2023-04-07 20:31:02 +00:00
// returns the macos_setup keyed by team name.
func extractTmSpecsMacOSSetup ( tmSpecs [ ] json . RawMessage ) map [ string ] * fleet . MacOSSetup {
var m map [ string ] * fleet . MacOSSetup
for _ , tm := range tmSpecs {
var spec struct {
Name string ` json:"name" `
MDM struct {
MacOSSetup fleet . MacOSSetup ` json:"macos_setup" `
} ` json:"mdm" `
}
if err := json . Unmarshal ( tm , & spec ) ; err != nil {
// ignore, this will fail in the call to apply team specs
continue
}
2024-02-27 18:55:05 +00:00
spec . Name = norm . NFC . String ( spec . Name )
2023-04-07 20:31:02 +00:00
if spec . Name != "" {
if m == nil {
m = make ( map [ string ] * fleet . MacOSSetup )
}
m [ spec . Name ] = & spec . MDM . MacOSSetup
}
}
return m
}
2024-02-09 19:34:57 +00:00
// DoGitOps applies the GitOps config to Fleet.
func ( c * Client ) DoGitOps (
ctx context . Context ,
config * spec . GitOps ,
2024-06-27 21:10:49 +00:00
fullFilename string ,
2024-02-09 19:34:57 +00:00
logf func ( format string , args ... interface { } ) ,
dryRun bool ,
2024-05-03 13:03:00 +00:00
teamDryRunAssumptions * fleet . TeamSpecsDryRunAssumptions ,
2024-03-07 19:20:14 +00:00
appConfig * fleet . EnrichedAppConfig ,
2024-10-10 11:12:24 +00:00
// pass-by-ref to build lists
teamsSoftwareInstallers map [ string ] [ ] fleet . SoftwarePackageResponse ,
teamsScripts map [ string ] [ ] fleet . ScriptResponse ,
2024-05-03 13:03:00 +00:00
) ( * fleet . TeamSpecsDryRunAssumptions , error ) {
2024-06-27 21:10:49 +00:00
baseDir := filepath . Dir ( fullFilename )
filename := filepath . Base ( fullFilename )
2024-05-03 13:03:00 +00:00
var teamAssumptions * fleet . TeamSpecsDryRunAssumptions
2024-02-09 19:34:57 +00:00
var err error
logFn := func ( format string , args ... interface { } ) {
if logf != nil {
logf ( format , args ... )
}
}
group := spec . Group { }
scripts := make ( [ ] interface { } , len ( config . Controls . Scripts ) )
for i , script := range config . Controls . Scripts {
scripts [ i ] = * script . Path
}
var mdmAppConfig map [ string ] interface { }
var team map [ string ] interface { }
if config . TeamName == nil {
group . AppConfig = config . OrgSettings
group . EnrollSecret = & fleet . EnrollSecretSpec { Secrets : config . OrgSettings [ "secrets" ] . ( [ ] * fleet . EnrollSecret ) }
group . AppConfig . ( map [ string ] interface { } ) [ "agent_options" ] = config . AgentOptions
delete ( config . OrgSettings , "secrets" ) // secrets are applied separately in Client.ApplyGroup
2024-03-13 15:05:46 +00:00
// Integrations
var integrations interface { }
var ok bool
if integrations , ok = group . AppConfig . ( map [ string ] interface { } ) [ "integrations" ] ; ! ok || integrations == nil {
integrations = map [ string ] interface { } { }
group . AppConfig . ( map [ string ] interface { } ) [ "integrations" ] = integrations
}
2024-10-16 16:12:48 +00:00
integrations , ok = integrations . ( map [ string ] interface { } )
if ! ok {
return nil , errors . New ( "org_settings.integrations config is not a map" )
}
2024-03-13 15:05:46 +00:00
if jira , ok := integrations . ( map [ string ] interface { } ) [ "jira" ] ; ! ok || jira == nil {
integrations . ( map [ string ] interface { } ) [ "jira" ] = [ ] interface { } { }
}
if zendesk , ok := integrations . ( map [ string ] interface { } ) [ "zendesk" ] ; ! ok || zendesk == nil {
integrations . ( map [ string ] interface { } ) [ "zendesk" ] = [ ] interface { } { }
2024-02-09 19:34:57 +00:00
}
2024-03-13 15:05:46 +00:00
if googleCal , ok := integrations . ( map [ string ] interface { } ) [ "google_calendar" ] ; ! ok || googleCal == nil {
integrations . ( map [ string ] interface { } ) [ "google_calendar" ] = [ ] interface { } { }
2024-02-09 19:34:57 +00:00
}
2024-10-16 16:12:48 +00:00
if ndesSCEPProxy , ok := integrations . ( map [ string ] interface { } ) [ "ndes_scep_proxy" ] ; ! ok || ndesSCEPProxy == nil {
// Per backend patterns.md, best practice is to clear a JSON config field with `null`
integrations . ( map [ string ] interface { } ) [ "ndes_scep_proxy" ] = nil
} else {
if _ , ok = ndesSCEPProxy . ( map [ string ] interface { } ) ; ! ok {
return nil , errors . New ( "org_settings.integrations.ndes_scep_proxy config is not a map" )
}
}
2024-03-13 15:05:46 +00:00
2024-02-12 23:27:48 +00:00
// Ensure mdm config exists
mdmConfig , ok := group . AppConfig . ( map [ string ] interface { } ) [ "mdm" ]
if ! ok || mdmConfig == nil {
mdmConfig = map [ string ] interface { } { }
group . AppConfig . ( map [ string ] interface { } ) [ "mdm" ] = mdmConfig
}
mdmAppConfig , ok = mdmConfig . ( map [ string ] interface { } )
if ! ok {
2024-05-03 13:03:00 +00:00
return nil , errors . New ( "org_settings.mdm config is not a map" )
2024-02-12 23:27:48 +00:00
}
2024-10-25 13:01:18 +00:00
// Put in default value for volume_purchasing_program to clear the configuration if it's not set.
if v , ok := mdmAppConfig [ "volume_purchasing_program" ] ; ! ok || v == nil {
mdmAppConfig [ "volume_purchasing_program" ] = [ ] interface { } { }
}
2024-03-07 19:20:14 +00:00
// Put in default values for macos_migration
if config . Controls . MacOSMigration != nil {
mdmAppConfig [ "macos_migration" ] = config . Controls . MacOSMigration
} else {
mdmAppConfig [ "macos_migration" ] = map [ string ] interface { } { }
}
macOSMigration := mdmAppConfig [ "macos_migration" ] . ( map [ string ] interface { } )
if enable , ok := macOSMigration [ "enable" ] ; ! ok || enable == nil {
macOSMigration [ "enable" ] = false
}
// Put in default values for windows_enabled_and_configured
2024-02-09 19:34:57 +00:00
mdmAppConfig [ "windows_enabled_and_configured" ] = config . Controls . WindowsEnabledAndConfigured
2024-03-07 19:20:14 +00:00
if config . Controls . WindowsEnabledAndConfigured != nil {
mdmAppConfig [ "windows_enabled_and_configured" ] = config . Controls . WindowsEnabledAndConfigured
} else {
mdmAppConfig [ "windows_enabled_and_configured" ] = false
}
2024-05-03 13:03:00 +00:00
if windowsEnabledAndConfiguredAssumption , ok := mdmAppConfig [ "windows_enabled_and_configured" ] . ( bool ) ; ok {
teamAssumptions = & fleet . TeamSpecsDryRunAssumptions {
WindowsEnabledAndConfigured : optjson . SetBool ( windowsEnabledAndConfiguredAssumption ) ,
}
}
2024-02-09 19:34:57 +00:00
group . AppConfig . ( map [ string ] interface { } ) [ "scripts" ] = scripts
2024-09-12 17:23:25 +00:00
} else if ! config . IsNoTeam ( ) {
2024-02-09 19:34:57 +00:00
team = make ( map [ string ] interface { } )
team [ "name" ] = * config . TeamName
team [ "agent_options" ] = config . AgentOptions
if hostExpirySettings , ok := config . TeamSettings [ "host_expiry_settings" ] ; ok {
team [ "host_expiry_settings" ] = hostExpirySettings
}
if features , ok := config . TeamSettings [ "features" ] ; ok {
team [ "features" ] = features
}
team [ "scripts" ] = scripts
2024-07-10 18:53:03 +00:00
team [ "software" ] = map [ string ] any { }
team [ "software" ] . ( map [ string ] any ) [ "app_store_apps" ] = config . Software . AppStoreApps
team [ "software" ] . ( map [ string ] any ) [ "packages" ] = config . Software . Packages
2024-02-09 19:34:57 +00:00
team [ "secrets" ] = config . TeamSettings [ "secrets" ]
Enabling setting host status webhook at the team level via REST API and fleetctl apply/gitops. (#17186)
Enabling setting host status webhook at the team level via REST API and
fleetctl apply/gitops.
#14916
Example payload:
```json
{
"data": {
"days_unseen": 3,
"host_ids": [
10724,
10726,
10738,
10739,
10740,
10741,
10742,
10744,
10745,
10746,
10747,
10748,
10749
],
"team_id": 3,
"total_hosts": 15,
"unseen_hosts": 13
},
"text": "More than 86.67% of your hosts have not checked into Fleet for more than 3 days. You've been sent this message because the Host status webhook is enabled in your Fleet instance."
}
```
# Checklist for submitter
- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
2024-03-04 18:35:27 +00:00
team [ "webhook_settings" ] = map [ string ] interface { } { }
clearHostStatusWebhook := true
if webhookSettings , ok := config . TeamSettings [ "webhook_settings" ] ; ok {
2024-08-30 21:00:35 +00:00
if _ , ok := webhookSettings . ( map [ string ] interface { } ) ; ok {
if hostStatusWebhook , ok := webhookSettings . ( map [ string ] interface { } ) [ "host_status_webhook" ] ; ok {
clearHostStatusWebhook = false
team [ "webhook_settings" ] . ( map [ string ] interface { } ) [ "host_status_webhook" ] = hostStatusWebhook
}
} else if webhookSettings != nil {
return nil , fmt . Errorf ( "team_settings.webhook_settings config is not a map but a %T" , webhookSettings )
Enabling setting host status webhook at the team level via REST API and fleetctl apply/gitops. (#17186)
Enabling setting host status webhook at the team level via REST API and
fleetctl apply/gitops.
#14916
Example payload:
```json
{
"data": {
"days_unseen": 3,
"host_ids": [
10724,
10726,
10738,
10739,
10740,
10741,
10742,
10744,
10745,
10746,
10747,
10748,
10749
],
"team_id": 3,
"total_hosts": 15,
"unseen_hosts": 13
},
"text": "More than 86.67% of your hosts have not checked into Fleet for more than 3 days. You've been sent this message because the Host status webhook is enabled in your Fleet instance."
}
```
# Checklist for submitter
- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
2024-03-04 18:35:27 +00:00
}
}
if clearHostStatusWebhook {
// Clear out any existing host_status_webhook settings
team [ "webhook_settings" ] . ( map [ string ] interface { } ) [ "host_status_webhook" ] = map [ string ] interface { } { }
}
2024-03-13 15:05:46 +00:00
// Integrations
var integrations interface { }
var ok bool
if integrations , ok = config . TeamSettings [ "integrations" ] ; ! ok || integrations == nil {
integrations = map [ string ] interface { } { }
}
team [ "integrations" ] = integrations
_ , ok = integrations . ( map [ string ] interface { } )
if ! ok {
2024-05-03 13:03:00 +00:00
return nil , errors . New ( "team_settings.integrations config is not a map" )
2024-03-13 15:05:46 +00:00
}
2024-03-15 00:00:51 +00:00
if googleCal , ok := integrations . ( map [ string ] interface { } ) [ "google_calendar" ] ; ! ok || googleCal == nil {
integrations . ( map [ string ] interface { } ) [ "google_calendar" ] = map [ string ] interface { } { }
} else {
_ , ok = googleCal . ( map [ string ] interface { } )
2024-03-13 15:05:46 +00:00
if ! ok {
2024-05-03 13:03:00 +00:00
return nil , errors . New ( "team_settings.integrations.google_calendar config is not a map" )
2024-03-13 15:05:46 +00:00
}
}
2024-02-09 19:34:57 +00:00
team [ "mdm" ] = map [ string ] interface { } { }
mdmAppConfig = team [ "mdm" ] . ( map [ string ] interface { } )
}
2024-09-12 17:23:25 +00:00
if ! config . IsNoTeam ( ) {
// Common controls settings between org and team settings
// Put in default values for macos_settings
if config . Controls . MacOSSettings != nil {
mdmAppConfig [ "macos_settings" ] = config . Controls . MacOSSettings
} else {
mdmAppConfig [ "macos_settings" ] = map [ string ] interface { } { }
2024-03-07 19:20:14 +00:00
}
2024-09-12 17:23:25 +00:00
macOSSettings := mdmAppConfig [ "macos_settings" ] . ( map [ string ] interface { } )
if customSettings , ok := macOSSettings [ "custom_settings" ] ; ! ok || customSettings == nil {
macOSSettings [ "custom_settings" ] = [ ] interface { } { }
2024-03-07 19:20:14 +00:00
}
2024-09-12 17:23:25 +00:00
// Put in default values for macos_updates
if config . Controls . MacOSUpdates != nil {
mdmAppConfig [ "macos_updates" ] = config . Controls . MacOSUpdates
} else {
mdmAppConfig [ "macos_updates" ] = map [ string ] interface { } { }
}
macOSUpdates := mdmAppConfig [ "macos_updates" ] . ( map [ string ] interface { } )
if minimumVersion , ok := macOSUpdates [ "minimum_version" ] ; ! ok || minimumVersion == nil {
macOSUpdates [ "minimum_version" ] = ""
}
if deadline , ok := macOSUpdates [ "deadline" ] ; ! ok || deadline == nil {
macOSUpdates [ "deadline" ] = ""
}
// Put in default values for ios_updates
if config . Controls . IOSUpdates != nil {
mdmAppConfig [ "ios_updates" ] = config . Controls . IOSUpdates
} else {
mdmAppConfig [ "ios_updates" ] = map [ string ] interface { } { }
}
iOSUpdates := mdmAppConfig [ "ios_updates" ] . ( map [ string ] interface { } )
if minimumVersion , ok := iOSUpdates [ "minimum_version" ] ; ! ok || minimumVersion == nil {
iOSUpdates [ "minimum_version" ] = ""
}
if deadline , ok := iOSUpdates [ "deadline" ] ; ! ok || deadline == nil {
iOSUpdates [ "deadline" ] = ""
}
// Put in default values for ipados_updates
if config . Controls . IPadOSUpdates != nil {
mdmAppConfig [ "ipados_updates" ] = config . Controls . IPadOSUpdates
} else {
mdmAppConfig [ "ipados_updates" ] = map [ string ] interface { } { }
}
iPadOSUpdates := mdmAppConfig [ "ipados_updates" ] . ( map [ string ] interface { } )
if minimumVersion , ok := iPadOSUpdates [ "minimum_version" ] ; ! ok || minimumVersion == nil {
iPadOSUpdates [ "minimum_version" ] = ""
}
if deadline , ok := iPadOSUpdates [ "deadline" ] ; ! ok || deadline == nil {
iPadOSUpdates [ "deadline" ] = ""
}
// Put in default values for macos_setup
if config . Controls . MacOSSetup != nil {
mdmAppConfig [ "macos_setup" ] = config . Controls . MacOSSetup
} else {
mdmAppConfig [ "macos_setup" ] = map [ string ] interface { } { }
}
macOSSetup := mdmAppConfig [ "macos_setup" ] . ( map [ string ] interface { } )
if bootstrapPackage , ok := macOSSetup [ "bootstrap_package" ] ; ! ok || bootstrapPackage == nil {
macOSSetup [ "bootstrap_package" ] = ""
}
if enableEndUserAuthentication , ok := macOSSetup [ "enable_end_user_authentication" ] ; ! ok || enableEndUserAuthentication == nil {
macOSSetup [ "enable_end_user_authentication" ] = false
}
if macOSSetupAssistant , ok := macOSSetup [ "macos_setup_assistant" ] ; ! ok || macOSSetupAssistant == nil {
macOSSetup [ "macos_setup_assistant" ] = ""
}
// Put in default values for windows_settings
if config . Controls . WindowsSettings != nil {
mdmAppConfig [ "windows_settings" ] = config . Controls . WindowsSettings
} else {
mdmAppConfig [ "windows_settings" ] = map [ string ] interface { } { }
}
windowsSettings := mdmAppConfig [ "windows_settings" ] . ( map [ string ] interface { } )
if customSettings , ok := windowsSettings [ "custom_settings" ] ; ! ok || customSettings == nil {
windowsSettings [ "custom_settings" ] = [ ] interface { } { }
}
// Put in default values for windows_updates
if config . Controls . WindowsUpdates != nil {
mdmAppConfig [ "windows_updates" ] = config . Controls . WindowsUpdates
} else {
mdmAppConfig [ "windows_updates" ] = map [ string ] interface { } { }
}
if appConfig . License . IsPremium ( ) {
windowsUpdates := mdmAppConfig [ "windows_updates" ] . ( map [ string ] interface { } )
if deadlineDays , ok := windowsUpdates [ "deadline_days" ] ; ! ok || deadlineDays == nil {
windowsUpdates [ "deadline_days" ] = nil
}
if gracePeriodDays , ok := windowsUpdates [ "grace_period_days" ] ; ! ok || gracePeriodDays == nil {
windowsUpdates [ "grace_period_days" ] = nil
}
}
// Put in default value for enable_disk_encryption
if config . Controls . EnableDiskEncryption != nil {
mdmAppConfig [ "enable_disk_encryption" ] = config . Controls . EnableDiskEncryption
} else {
mdmAppConfig [ "enable_disk_encryption" ] = false
}
if config . TeamName != nil {
team [ "gitops_filename" ] = filename
rawTeam , err := json . Marshal ( team )
if err != nil {
return nil , fmt . Errorf ( "error marshalling team spec: %w" , err )
}
group . Teams = [ ] json . RawMessage { rawTeam }
group . TeamsDryRunAssumptions = teamDryRunAssumptions
2024-02-09 19:34:57 +00:00
}
}
2024-09-06 22:10:28 +00:00
// Apply org settings, scripts, enroll secrets, team entities (software, scripts, etc.), and controls.
2024-10-23 18:51:02 +00:00
teamIDsByName , teamsSoftwareInstallers , teamsScripts , err := c . ApplyGroup ( ctx , true , & group , baseDir , logf , appConfig , fleet . ApplyClientSpecOptions {
2024-05-28 16:44:43 +00:00
ApplySpecOptions : fleet . ApplySpecOptions {
DryRun : dryRun ,
} ,
ExpandEnvConfigProfiles : true ,
2024-10-10 11:12:24 +00:00
} , teamsSoftwareInstallers , teamsScripts )
2024-02-09 19:34:57 +00:00
if err != nil {
2024-05-03 13:03:00 +00:00
return nil , err
2024-02-09 19:34:57 +00:00
}
2024-09-12 17:23:25 +00:00
2024-09-17 16:30:27 +00:00
var teamSoftwareInstallers [ ] fleet . SoftwarePackageResponse
2024-10-04 01:03:40 +00:00
var teamScripts [ ] fleet . ScriptResponse
2024-02-09 19:34:57 +00:00
if config . TeamName != nil {
2024-09-12 17:23:25 +00:00
if ! config . IsNoTeam ( ) {
if len ( teamIDsByName ) != 1 {
return nil , fmt . Errorf ( "expected 1 team spec to be applied, got %d" , len ( teamIDsByName ) )
}
teamID , ok := teamIDsByName [ * config . TeamName ]
if ok && teamID == 0 {
if dryRun {
logFn ( "[+] would've added any policies/queries to new team %s\n" , * config . TeamName )
return nil , nil
}
return nil , fmt . Errorf ( "team %s not created" , * config . TeamName )
2024-02-09 19:34:57 +00:00
}
2024-09-12 17:23:25 +00:00
for _ , teamID = range teamIDsByName {
config . TeamID = & teamID
}
teamSoftwareInstallers = teamsSoftwareInstallers [ * config . TeamName ]
2024-10-04 01:03:40 +00:00
teamScripts = teamsScripts [ * config . TeamName ]
2024-09-12 17:23:25 +00:00
} else {
noTeamSoftwareInstallers , err := c . doGitOpsNoTeamSoftware ( config , baseDir , appConfig , logFn , dryRun )
if err != nil {
return nil , err
}
teamSoftwareInstallers = noTeamSoftwareInstallers
2024-10-04 01:03:40 +00:00
teamScripts = teamsScripts [ "No team" ]
2024-06-27 21:10:49 +00:00
}
2024-02-09 19:34:57 +00:00
}
2024-03-13 15:05:46 +00:00
2024-10-04 01:03:40 +00:00
err = c . doGitOpsPolicies ( config , teamSoftwareInstallers , teamScripts , logFn , dryRun )
2024-02-09 19:34:57 +00:00
if err != nil {
2024-05-03 13:03:00 +00:00
return nil , err
2024-02-09 19:34:57 +00:00
}
2024-09-18 17:16:59 +00:00
// We currently don't support queries for "No team" thus
// we just do GitOps for queries for global and team files.
if ! config . IsNoTeam ( ) {
err = c . doGitOpsQueries ( config , logFn , dryRun )
if err != nil {
return nil , err
}
2024-08-05 17:39:10 +00:00
}
2024-05-03 13:03:00 +00:00
return teamAssumptions , nil
2024-02-09 19:34:57 +00:00
}
2024-10-23 18:51:02 +00:00
func ( c * Client ) doGitOpsNoTeamSoftware (
config * spec . GitOps ,
baseDir string ,
appconfig * fleet . EnrichedAppConfig ,
logFn func ( format string , args ... interface { } ) ,
dryRun bool ,
) ( [ ] fleet . SoftwarePackageResponse , error ) {
if ! config . IsNoTeam ( ) || appconfig == nil || ! appconfig . License . IsPremium ( ) {
return nil , nil
}
// marshaling dance to get the macos_setup data - config.Controls.MacOSSetup
// is of type any and contains a generic map[string]any. By
// marshal-unmarshaling it into a properly typed struct, we avoid having to
// do a bunch of error-prone and unmaintainable type-assertions to walk down
// the untyped map.
b , err := json . Marshal ( config . Controls . MacOSSetup )
if err != nil {
return nil , fmt . Errorf ( "applying software installers: json-encode controls.macos_setup: %w" , err )
}
var macOSSetup fleet . MacOSSetup
if err := json . Unmarshal ( b , & macOSSetup ) ; err != nil {
return nil , fmt . Errorf ( "applying software installers: json-decode controls.macos_setup: %w" , err )
}
// load the no-team macos_setup.script if any
var macosSetupScript * fileContent
if macOSSetup . Script . Value != "" {
b , err := c . validateMacOSSetupScript ( resolveApplyRelativePath ( baseDir , macOSSetup . Script . Value ) )
2024-09-06 22:10:28 +00:00
if err != nil {
2024-10-23 18:51:02 +00:00
return nil , fmt . Errorf ( "applying no team macos_setup.script: %w" , err )
2024-08-05 17:39:10 +00:00
}
2024-10-23 18:51:02 +00:00
macosSetupScript = & fileContent { Filename : filepath . Base ( macOSSetup . Script . Value ) , Content : b }
}
2024-08-05 17:39:10 +00:00
2024-10-23 18:51:02 +00:00
noTeamSoftwareMacOSSetup , err := extractTeamOrNoTeamMacOSSetupSoftware ( baseDir , macOSSetup . Software . Value )
if err != nil {
return nil , err
}
2024-10-28 14:35:57 +00:00
var softwareInstallers [ ] fleet . SoftwarePackageResponse
packages := make ( [ ] fleet . SoftwarePackageSpec , 0 , len ( config . Software . Packages ) )
packagesByPath := make ( map [ string ] fleet . SoftwarePackageSpec , len ( config . Software . Packages ) )
for _ , software := range config . Software . Packages {
if software != nil {
packages = append ( packages , * software )
if software . ReferencedYamlPath != "" {
// can be referenced by macos_setup.software
packagesByPath [ software . ReferencedYamlPath ] = * software
}
}
}
var appsPayload [ ] fleet . VPPBatchPayload
appsByAppID := make ( map [ string ] fleet . TeamSpecAppStoreApp , len ( config . Software . AppStoreApps ) )
for _ , vppApp := range config . Software . AppStoreApps {
if vppApp != nil {
// can be referenced by macos_setup.software
appsByAppID [ vppApp . AppStoreID ] = * vppApp
_ , installDuringSetup := noTeamSoftwareMacOSSetup [ fleet . MacOSSetupSoftware { AppStoreID : vppApp . AppStoreID } ]
appsPayload = append ( appsPayload , fleet . VPPBatchPayload {
AppStoreID : vppApp . AppStoreID ,
SelfService : vppApp . SelfService ,
InstallDuringSetup : & installDuringSetup ,
} )
}
}
if err := validateTeamOrNoTeamMacOSSetupSoftware ( * config . TeamName , macOSSetup . Software . Value , packagesByPath , appsByAppID ) ; err != nil {
2024-10-23 18:51:02 +00:00
return nil , err
}
2024-11-07 17:22:08 +00:00
swPkgPayload , err := buildSoftwarePackagesPayload ( packages , noTeamSoftwareMacOSSetup )
2024-10-23 18:51:02 +00:00
if err != nil {
return nil , fmt . Errorf ( "applying software installers: %w" , err )
}
if macosSetupScript != nil {
logFn ( "[+] applying macos setup experience script for 'No team'\n" )
if err := c . uploadMacOSSetupScript ( macosSetupScript . Filename , macosSetupScript . Content , nil ) ; err != nil {
return nil , fmt . Errorf ( "uploading setup experience script for No team: %w" , err )
2024-08-05 17:39:10 +00:00
}
2024-10-23 18:51:02 +00:00
} else if err := c . deleteMacOSSetupScript ( nil ) ; err != nil {
return nil , fmt . Errorf ( "deleting setup experience script for No team: %w" , err )
}
2024-10-28 14:35:57 +00:00
logFn ( "[+] applying %d software packages for 'No team'\n" , len ( swPkgPayload ) )
softwareInstallers , err = c . ApplyNoTeamSoftwareInstallers ( swPkgPayload , fleet . ApplySpecOptions { DryRun : dryRun } )
2024-10-23 18:51:02 +00:00
if err != nil {
return nil , fmt . Errorf ( "applying software installers: %w" , err )
}
2024-10-28 14:35:57 +00:00
logFn ( "[+] applying %d app store apps for 'No team'\n" , len ( appsPayload ) )
if err := c . ApplyNoTeamAppStoreAppsAssociation ( appsPayload , fleet . ApplySpecOptions { DryRun : dryRun } ) ; err != nil {
return nil , fmt . Errorf ( "applying app store apps: %w" , err )
}
2024-10-23 18:51:02 +00:00
if dryRun {
logFn ( "[+] would've applied 'No Team' software packages\n" )
} else {
logFn ( "[+] applied 'No Team' software packages\n" )
2024-08-05 17:39:10 +00:00
}
2024-09-06 22:10:28 +00:00
return softwareInstallers , nil
2024-08-05 17:39:10 +00:00
}
2024-10-04 01:03:40 +00:00
func ( c * Client ) doGitOpsPolicies ( config * spec . GitOps , teamSoftwareInstallers [ ] fleet . SoftwarePackageResponse , teamScripts [ ] fleet . ScriptResponse , logFn func ( format string , args ... interface { } ) , dryRun bool ) error {
2024-09-12 17:23:25 +00:00
var teamID * uint // Global policies (nil)
switch {
case config . TeamID != nil : // Team policies
teamID = config . TeamID
case config . IsNoTeam ( ) : // "No team" policies
teamID = ptr . Uint ( 0 )
}
if teamID != nil {
2024-10-04 01:03:40 +00:00
// Get software titles of packages for the team.
2024-09-06 22:10:28 +00:00
softwareTitleURLs := make ( map [ string ] uint )
for _ , softwareInstaller := range teamSoftwareInstallers {
if softwareInstaller . TitleID == nil {
// Should not happen, but to not panic we just log a warning.
2024-09-17 16:30:27 +00:00
logFn ( "[!] software installer without title id: team_id=%d, url=%s\n" , * teamID , softwareInstaller . URL )
continue
}
if softwareInstaller . URL == "" {
// Should not happen because we previously applied packages via gitops, but to not panic we just log a warning.
logFn ( "[!] software installer without url: team_id=%d, title_id=%d\n" , * teamID , * softwareInstaller . TitleID )
2024-09-06 22:10:28 +00:00
continue
}
softwareTitleURLs [ softwareInstaller . URL ] = * softwareInstaller . TitleID
}
for i := range config . Policies {
config . Policies [ i ] . SoftwareTitleID = ptr . Uint ( 0 ) // 0 unsets the installer
if config . Policies [ i ] . InstallSoftware == nil {
continue
}
softwareTitleID , ok := softwareTitleURLs [ config . Policies [ i ] . InstallSoftwareURL ]
if ! ok {
// Should not happen because software packages are uploaded first.
if ! dryRun {
logFn ( "[!] software URL without software title id: %s\n" , config . Policies [ i ] . InstallSoftwareURL )
}
continue
}
config . Policies [ i ] . SoftwareTitleID = & softwareTitleID
}
2024-10-04 01:03:40 +00:00
// Get scripts for the team.
scriptIDsByName := make ( map [ string ] uint )
for _ , script := range teamScripts {
scriptIDsByName [ script . Name ] = script . ID
}
for i := range config . Policies {
config . Policies [ i ] . ScriptID = ptr . Uint ( 0 ) // 0 unsets the script
if config . Policies [ i ] . RunScript == nil {
continue
}
scriptID , ok := scriptIDsByName [ * config . Policies [ i ] . RunScriptName ]
if ! ok {
if ! dryRun { // this shouldn't happen
logFn ( "[!] reference to an unknown script: %s\n" , * config . Policies [ i ] . RunScriptName )
}
continue
}
config . Policies [ i ] . ScriptID = & scriptID
}
2024-09-06 22:10:28 +00:00
}
2024-02-09 19:34:57 +00:00
// Get the ids and names of current policies to figure out which ones to delete
2024-09-12 17:23:25 +00:00
policies , err := c . GetPolicies ( teamID )
2024-02-09 19:34:57 +00:00
if err != nil {
return fmt . Errorf ( "error getting current policies: %w" , err )
}
2024-09-06 22:10:28 +00:00
2024-02-09 19:34:57 +00:00
if len ( config . Policies ) > 0 {
numPolicies := len ( config . Policies )
logFn ( "[+] syncing %d policies\n" , numPolicies )
if ! dryRun {
2024-03-28 17:39:27 +00:00
totalApplied := 0
for i := 0 ; i < len ( config . Policies ) ; i += batchSize {
end := i + batchSize
if end > len ( config . Policies ) {
end = len ( config . Policies )
}
totalApplied += end - i
// Note: We are reusing the spec flow here for adding/updating policies, instead of creating a new flow for GitOps.
2024-09-06 22:10:28 +00:00
policiesToApply := config . Policies [ i : end ]
policiesSpec := make ( [ ] * fleet . PolicySpec , len ( policiesToApply ) )
for i := range policiesToApply {
policiesSpec [ i ] = & policiesToApply [ i ] . PolicySpec
}
if err := c . ApplyPolicies ( policiesSpec ) ; err != nil {
2024-03-28 17:39:27 +00:00
return fmt . Errorf ( "error applying policies: %w" , err )
}
logFn ( "[+] synced %d policies\n" , totalApplied )
2024-02-09 19:34:57 +00:00
}
}
}
var policiesToDelete [ ] uint
for _ , oldItem := range policies {
found := false
for _ , newItem := range config . Policies {
if oldItem . Name == newItem . Name {
found = true
break
}
}
if ! found {
policiesToDelete = append ( policiesToDelete , oldItem . ID )
2024-09-12 17:23:25 +00:00
if ! dryRun {
logFn ( "[-] deleting policy %s\n" , oldItem . Name )
} else {
logFn ( "[-] would've deleted policy %s\n" , oldItem . Name )
}
2024-02-09 19:34:57 +00:00
}
}
if len ( policiesToDelete ) > 0 {
logFn ( "[-] deleting %d policies\n" , len ( policiesToDelete ) )
if ! dryRun {
2024-03-28 17:39:27 +00:00
totalDeleted := 0
for i := 0 ; i < len ( policiesToDelete ) ; i += batchSize {
end := i + batchSize
if end > len ( policiesToDelete ) {
end = len ( policiesToDelete )
}
totalDeleted += end - i
2024-09-12 17:23:25 +00:00
var teamID * uint
switch {
case config . TeamID != nil : // Team policies
teamID = config . TeamID
case config . IsNoTeam ( ) : // No team policies
teamID = ptr . Uint ( fleet . PolicyNoTeamID )
default : // Global policies
teamID = nil
}
if err := c . DeletePolicies ( teamID , policiesToDelete [ i : end ] ) ; err != nil {
2024-03-28 17:39:27 +00:00
return fmt . Errorf ( "error deleting policies: %w" , err )
}
logFn ( "[-] deleted %d policies\n" , totalDeleted )
2024-02-09 19:34:57 +00:00
}
}
}
return nil
}
func ( c * Client ) doGitOpsQueries ( config * spec . GitOps , logFn func ( format string , args ... interface { } ) , dryRun bool ) error {
2024-03-28 17:39:27 +00:00
batchSize := 100
2024-02-09 19:34:57 +00:00
// Get the ids and names of current queries to figure out which ones to delete
queries , err := c . GetQueries ( config . TeamID , nil )
if err != nil {
return fmt . Errorf ( "error getting current queries: %w" , err )
}
if len ( config . Queries ) > 0 {
numQueries := len ( config . Queries )
logFn ( "[+] syncing %d queries\n" , numQueries )
if ! dryRun {
2024-03-28 17:39:27 +00:00
appliedCount := 0
for i := 0 ; i < len ( config . Queries ) ; i += batchSize {
end := i + batchSize
if end > len ( config . Queries ) {
end = len ( config . Queries )
}
appliedCount += end - i
// Note: We are reusing the spec flow here for adding/updating queries, instead of creating a new flow for GitOps.
if err := c . ApplyQueries ( config . Queries [ i : end ] ) ; err != nil {
return fmt . Errorf ( "error applying queries: %w" , err )
}
logFn ( "[+] synced %d queries\n" , appliedCount )
2024-02-09 19:34:57 +00:00
}
}
}
var queriesToDelete [ ] uint
for _ , oldQuery := range queries {
found := false
for _ , newQuery := range config . Queries {
if oldQuery . Name == newQuery . Name {
found = true
break
}
}
if ! found {
queriesToDelete = append ( queriesToDelete , oldQuery . ID )
2024-10-25 18:35:53 +00:00
if ! dryRun {
fmt . Printf ( "[-] deleting query %s\n" , oldQuery . Name )
} else {
fmt . Printf ( "[-] would've deleted query %s\n" , oldQuery . Name )
}
2024-02-09 19:34:57 +00:00
}
}
if len ( queriesToDelete ) > 0 {
logFn ( "[-] deleting %d queries\n" , len ( queriesToDelete ) )
if ! dryRun {
2024-03-28 17:39:27 +00:00
deleteCount := 0
for i := 0 ; i < len ( queriesToDelete ) ; i += batchSize {
end := i + batchSize
if end > len ( queriesToDelete ) {
end = len ( queriesToDelete )
}
deleteCount += end - i
if err := c . DeleteQueries ( queriesToDelete [ i : end ] ) ; err != nil {
return fmt . Errorf ( "error deleting queries: %w" , err )
}
logFn ( "[-] deleted %d queries\n" , deleteCount )
2024-02-09 19:34:57 +00:00
}
}
}
return nil
}
2024-05-31 12:01:13 +00:00
func ( c * Client ) GetGitOpsSecrets (
config * spec . GitOps ,
) [ ] string {
if config . TeamName == nil {
orgSecrets , ok := config . OrgSettings [ "secrets" ]
if ok {
secrets , ok := orgSecrets . ( [ ] * fleet . EnrollSecret )
if ok {
secretValues := make ( [ ] string , 0 , len ( secrets ) )
for _ , secret := range secrets {
secretValues = append ( secretValues , secret . Secret )
}
return secretValues
}
}
} else {
teamSecrets , ok := config . TeamSettings [ "secrets" ]
if ok {
secrets , ok := teamSecrets . ( [ ] * fleet . EnrollSecret )
if ok {
secretValues := make ( [ ] string , 0 , len ( secrets ) )
for _ , secret := range secrets {
secretValues = append ( secretValues , secret . Secret )
}
return secretValues
}
}
}
return nil
}