Allow team gitops to run without global config (#26969)

#26171
This commit is contained in:
Dante Catalfamo
2025-03-18 12:24:35 -04:00
committed by GitHub
parent fb490c7450
commit c5bffe7c0d
7 changed files with 47 additions and 24 deletions
+1
View File
@@ -0,0 +1 @@
- Allow team gitops to run without global config
+33 -15
View File
@@ -9,7 +9,6 @@ import (
"github.com/fleetdm/fleet/v4/pkg/spec"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/urfave/cli/v2"
"golang.org/x/text/unicode/norm"
@@ -33,7 +32,7 @@ func gitopsCommand() *cli.Command {
Required: true,
EnvVars: []string{"FILENAME"},
Destination: &flFilenames,
Usage: "The file(s) with the GitOps configuration. If multiple files are provided, the first file must be the global configuration and the rest must be team configurations.",
Usage: "The file(s) with the GitOps configuration.",
},
&cli.BoolFlag{
Name: "delete-other-teams",
@@ -90,13 +89,9 @@ func gitopsCommand() *cli.Command {
var originalABMConfig []any
var originalVPPConfig []any
var teamNames []string
var firstFileMustBeGlobal *bool
var teamDryRunAssumptions *fleet.TeamSpecsDryRunAssumptions
var abmTeams, vppTeams []string
var hasMissingABMTeam, hasMissingVPPTeam, usesLegacyABMConfig bool
if totalFilenames > 1 {
firstFileMustBeGlobal = ptr.Bool(true)
}
// we keep track of team software installers and scripts for correct policy application
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
@@ -107,6 +102,20 @@ func gitopsCommand() *cli.Command {
secrets := make(map[string]struct{})
// We keep track of the environment FLEET_SECRET_* variables
allFleetSecrets := make(map[string]string)
// Parsed config and filename pair
type ConfigFile struct {
Config *spec.GitOps
Filename string
IsGlobalConfig bool
}
// Load all configs in before processing them
configs := make([]ConfigFile, 0, len(flFilenames.Value()))
// We only want to have one global config loaded
globalConfigLoaded := false
for _, flFilename := range flFilenames.Value() {
baseDir := filepath.Dir(flFilename)
config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, logf)
@@ -114,17 +123,26 @@ func gitopsCommand() *cli.Command {
return err
}
isGlobalConfig := config.TeamName == nil
if firstFileMustBeGlobal != nil {
switch {
case *firstFileMustBeGlobal && !isGlobalConfig:
return fmt.Errorf("first file %s must be the global config", flFilename)
case !*firstFileMustBeGlobal && isGlobalConfig:
return fmt.Errorf(
"the file %s cannot be the global config, only the first file can be the global config", flFilename,
)
if isGlobalConfig {
if globalConfigLoaded {
return errors.New("only one global config file may be provided to fleetctl gitops")
}
firstFileMustBeGlobal = ptr.Bool(false)
globalConfigLoaded = true
}
configFile := ConfigFile{Config: config, Filename: flFilename, IsGlobalConfig: isGlobalConfig}
if isGlobalConfig {
// If it's a global file, put it at the beginning
// of the array so it gets processed first
configs = append([]ConfigFile{configFile}, configs...)
} else {
configs = append(configs, configFile)
}
}
for _, configFile := range configs {
config := configFile.Config
flFilename := configFile.Filename
isGlobalConfig := configFile.IsGlobalConfig
if isGlobalConfig {
if noTeamControls.Set() && config.Controls.Set() {
+7 -3
View File
@@ -1365,13 +1365,17 @@ software:
// Files out of order
_, err = runAppNoChecks([]string{"gitops", "-f", teamFile.Name(), "-f", globalFile.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "must be the global config"))
require.NoError(t, err)
// No global file, only team file
_, err = runAppNoChecks([]string{"gitops", "-f", teamFile.Name(), "--dry-run"})
require.NoError(t, err)
// Global file specified multiple times
_, err = runAppNoChecks([]string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "-f", globalFile.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "only the first file can be the global config"))
fmt.Printf("err.Error(): %v\n", err.Error())
assert.Contains(t, err.Error(), "only one global config file may be provided")
// Duplicate secret
_, err = runAppNoChecks([]string{"gitops", "-f", globalFile.Name(), "-f", teamFileDupSecret.Name(), "--dry-run"})
+1 -1
View File
@@ -176,7 +176,7 @@ func TestCountABMTokensAuth(t *testing.T) {
{"observer can read", test.UserObserver, false},
{"observer+ can read", test.UserObserverPlus, false},
{"admin can read", test.UserAdmin, false},
{"tm1 gitops cannot read", test.UserTeamGitOpsTeam1, true},
{"tm1 gitops can read", test.UserTeamGitOpsTeam1, false},
{"tm1 maintainer can read", test.UserTeamMaintainerTeam1, false},
{"tm1 observer can read", test.UserTeamObserverTeam1, false},
{"tm1 observer+ can read", test.UserTeamObserverPlusTeam1, false},
+2 -2
View File
@@ -61,8 +61,8 @@ allow {
# Team admin, maintainer, observer_plus and observer can read global config.
allow {
object.type == "app_config"
# If role is admin, maintainer, observer_plus or observer on any team.
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_]
# If role is admin, gitops, maintainer, observer_plus or observer on any team.
team_role(subject, subject.teams[_].id) == [admin, gitops, maintainer, observer_plus, observer][_]
action == read
}
+1 -1
View File
@@ -80,7 +80,7 @@ func TestAuthorizeAppConfig(t *testing.T) {
{user: test.UserTeamObserverPlusTeam1, object: config, action: read, allow: true},
{user: test.UserTeamObserverPlusTeam1, object: config, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: config, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: config, action: read, allow: true},
{user: test.UserTeamGitOpsTeam1, object: config, action: write, allow: false},
})
}
+2 -2
View File
@@ -133,7 +133,7 @@ func TestAppConfigAuth(t *testing.T) {
"team gitops",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
true,
true,
false,
},
{
"user without roles",
@@ -610,7 +610,7 @@ func TestAppConfigSecretsObfuscated(t *testing.T) {
{
"team gitops",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
true,
false,
},
{
"user without roles",