feat: allow limiting clusterResourceWhitelist by resource name (#12208) (#24674)

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
This commit is contained in:
Michael Crenshaw
2025-12-03 15:55:28 -05:00
committed by GitHub
parent c43088265e
commit e77acec858
42 changed files with 2909 additions and 1042 deletions

View File

@@ -84,7 +84,7 @@ func newAppProject() *unstructured.Unstructured {
Server: "*",
},
},
ClusterResourceWhitelist: []metav1.GroupKind{
ClusterResourceWhitelist: []v1alpha1.ClusterResourceRestrictionItem{
{
Group: "*",
Kind: "*",

View File

@@ -591,17 +591,15 @@ func NewProjectRemoveSourceNamespace(clientOpts *argocdclient.ClientOptions) *co
return command
}
func modifyResourcesList(list *[]metav1.GroupKind, add bool, listDesc string, group string, kind string) bool {
func modifyNamespacedResourcesList(list *[]metav1.GroupKind, add bool, listAction string, group string, kind string) (bool, string) {
if add {
for _, item := range *list {
if item.Group == group && item.Kind == kind {
fmt.Printf("Group '%s' and kind '%s' already present in %s resources\n", group, kind, listDesc)
return false
return false, fmt.Sprintf("Group '%s' and kind '%s' already present in %s namespaced resources", group, kind, listAction)
}
}
fmt.Printf("Group '%s' and kind '%s' is added to %s resources\n", group, kind, listDesc)
*list = append(*list, metav1.GroupKind{Group: group, Kind: kind})
return true
return true, fmt.Sprintf("Group '%s' and kind '%s' is added to %s namespaced resources", group, kind, listAction)
}
index := -1
for i, item := range *list {
@@ -611,15 +609,37 @@ func modifyResourcesList(list *[]metav1.GroupKind, add bool, listDesc string, gr
}
}
if index == -1 {
fmt.Printf("Group '%s' and kind '%s' not in %s resources\n", group, kind, listDesc)
return false
return false, fmt.Sprintf("Group '%s' and kind '%s' not in %s namespaced resources", group, kind, listAction)
}
*list = append((*list)[:index], (*list)[index+1:]...)
fmt.Printf("Group '%s' and kind '%s' is removed from %s resources\n", group, kind, listDesc)
return true
return true, fmt.Sprintf("Group '%s' and kind '%s' is removed from %s namespaced resources", group, kind, listAction)
}
func modifyResourceListCmd(cmdUse, cmdDesc, examples string, clientOpts *argocdclient.ClientOptions, allow bool, namespacedList bool) *cobra.Command {
func modifyClusterResourcesList(list *[]v1alpha1.ClusterResourceRestrictionItem, add bool, listAction string, group string, kind string, name string) (bool, string) {
if add {
for _, item := range *list {
if item.Group == group && item.Kind == kind && item.Name == name {
return false, fmt.Sprintf("Group '%s', kind '%s', and name '%s' is already present in %s cluster resources", group, kind, name, listAction)
}
}
*list = append(*list, v1alpha1.ClusterResourceRestrictionItem{Group: group, Kind: kind, Name: name})
return true, fmt.Sprintf("Group '%s', kind '%s', and name '%s' is added to %s cluster resources", group, kind, name, listAction)
}
index := -1
for i, item := range *list {
if item.Group == group && item.Kind == kind && item.Name == name {
index = i
break
}
}
if index == -1 {
return false, fmt.Sprintf("Group '%s', kind '%s', and name '%s' not in %s cluster resources", group, kind, name, listAction)
}
*list = append((*list)[:index], (*list)[index+1:]...)
return true, fmt.Sprintf("Group '%s', kind '%s', and name '%s' is removed from %s cluster resources", group, kind, name, listAction)
}
func modifyResourceListCmd(getProjIf func(*cobra.Command) (io.Closer, projectpkg.ProjectServiceClient), cmdUse, cmdDesc, examples string, allow bool, namespacedList bool) *cobra.Command {
var (
listType string
defaultList string
@@ -636,38 +656,61 @@ func modifyResourceListCmd(cmdUse, cmdDesc, examples string, clientOpts *argocdc
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 3 {
if namespacedList && len(args) != 3 {
c.HelpFunc()(c, args)
os.Exit(1)
}
if !namespacedList && (len(args) < 3 || len(args) > 4) {
// Cluster-scoped resource command can have an optional NAME argument.
c.HelpFunc()(c, args)
os.Exit(1)
}
projName, group, kind := args[0], args[1], args[2]
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
var name string
if !namespacedList && len(args) > 3 {
name = args[3]
}
conn, projIf := getProjIf(c)
defer utilio.Close(conn)
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
var list, allowList, denyList *[]metav1.GroupKind
var listAction, listDesc string
var clusterList *[]v1alpha1.ClusterResourceRestrictionItem
var clusterAllowList, clusterDenyList *[]v1alpha1.ClusterResourceRestrictionItem
var listAction string
var add bool
if namespacedList {
allowList, denyList = &proj.Spec.NamespaceResourceWhitelist, &proj.Spec.NamespaceResourceBlacklist
listDesc = "namespaced"
} else {
allowList, denyList = &proj.Spec.ClusterResourceWhitelist, &proj.Spec.ClusterResourceBlacklist
listDesc = "cluster"
clusterAllowList, clusterDenyList = &proj.Spec.ClusterResourceWhitelist, &proj.Spec.ClusterResourceBlacklist
}
if (listType == "allow") || (listType == "white") {
list = allowList
clusterList = clusterAllowList
listAction = "allowed"
add = allow
} else {
list = denyList
clusterList = clusterDenyList
listAction = "denied"
add = !allow
}
if modifyResourcesList(list, add, listAction+" "+listDesc, group, kind) {
if !namespacedList {
if ok, msg := modifyClusterResourcesList(clusterList, add, listAction, group, kind, name); ok {
c.Println(msg)
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
return
}
if ok, msg := modifyNamespacedResourcesList(list, add, listAction, group, kind); ok {
c.Println(msg)
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
@@ -685,7 +728,10 @@ func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOpti
# Removes a namespaced API resource with specified GROUP and KIND from the deny list or add a namespaced API resource to the allow list for project PROJECT
argocd proj allow-namespace-resource PROJECT GROUP KIND
`
return modifyResourceListCmd(use, desc, examples, clientOpts, true, true)
getProjIf := func(cmd *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return headless.NewClientOrDie(clientOpts, cmd).NewProjectClientOrDie()
}
return modifyResourceListCmd(getProjIf, use, desc, examples, true, true)
}
// NewProjectDenyNamespaceResourceCommand returns a new instance of an `argocd proj deny-namespace-resource` command
@@ -696,7 +742,10 @@ func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptio
# Adds a namespaced API resource with specified GROUP and KIND from the deny list or removes a namespaced API resource from the allow list for project PROJECT
argocd proj deny-namespace-resource PROJECT GROUP KIND
`
return modifyResourceListCmd(use, desc, examples, clientOpts, false, true)
getProjIf := func(cmd *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return headless.NewClientOrDie(clientOpts, cmd).NewProjectClientOrDie()
}
return modifyResourceListCmd(getProjIf, use, desc, examples, false, true)
}
// NewProjectDenyClusterResourceCommand returns a new instance of an `deny-cluster-resource` command
@@ -707,18 +756,27 @@ func NewProjectDenyClusterResourceCommand(clientOpts *argocdclient.ClientOptions
# Removes a cluster-scoped API resource with specified GROUP and KIND from the allow list and adds it to deny list for project PROJECT
argocd proj deny-cluster-resource PROJECT GROUP KIND
`
return modifyResourceListCmd(use, desc, examples, clientOpts, false, false)
getProjIf := func(cmd *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return headless.NewClientOrDie(clientOpts, cmd).NewProjectClientOrDie()
}
return modifyResourceListCmd(getProjIf, use, desc, examples, false, false)
}
// NewProjectAllowClusterResourceCommand returns a new instance of an `argocd proj allow-cluster-resource` command
func NewProjectAllowClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
use := "allow-cluster-resource PROJECT GROUP KIND"
use := "allow-cluster-resource PROJECT GROUP KIND [NAME]"
desc := "Adds a cluster-scoped API resource to the allow list and removes it from deny list"
examples := `
# Adds a cluster-scoped API resource with specified GROUP and KIND to the allow list and removes it from deny list for project PROJECT
argocd proj allow-cluster-resource PROJECT GROUP KIND
# Adds a cluster-scoped API resource with specified GROUP, KIND and NAME pattern to the allow list and removes it from deny list for project PROJECT
argocd proj allow-cluster-resource PROJECT GROUP KIND NAME
`
return modifyResourceListCmd(use, desc, examples, clientOpts, true, false)
getProjIf := func(cmd *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return headless.NewClientOrDie(clientOpts, cmd).NewProjectClientOrDie()
}
return modifyResourceListCmd(getProjIf, use, desc, examples, true, false)
}
// NewProjectRemoveSourceCommand returns a new instance of an `argocd proj remove-src` command

View File

@@ -0,0 +1,256 @@
package commands
import (
"bytes"
"io"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
projectpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
projectmocks "github.com/argoproj/argo-cd/v3/pkg/apiclient/project/mocks"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)
func TestModifyResourceListCmd_AddClusterAllowItemWithName(t *testing.T) {
// Create a mock project client
mockProjClient := projectmocks.NewProjectServiceClient(t)
// Mock project data
projectName := "test-project"
mockProject := &v1alpha1.AppProject{
Spec: v1alpha1.AppProjectSpec{
ClusterResourceWhitelist: []v1alpha1.ClusterResourceRestrictionItem{},
},
}
// Mock Get and Update calls
mockProjClient.On("Get", mock.Anything, mock.Anything).Return(mockProject, nil)
mockProjClient.On("Update", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
req := args.Get(1).(*projectpkg.ProjectUpdateRequest)
mockProject.Spec.ClusterResourceWhitelist = req.Project.Spec.ClusterResourceWhitelist
}).Return(mockProject, nil)
getProjIf := func(_ *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return io.NopCloser(bytes.NewBufferString("")), mockProjClient
}
// Create the command
cmd := modifyResourceListCmd(
getProjIf,
"allow-cluster-resource",
"Test command",
"Example usage",
true,
false,
)
// Set up the command arguments
args := []string{projectName, "apps", "Deployment", "example-deployment"}
cmd.SetArgs(args)
// Capture the output
var output bytes.Buffer
cmd.SetOut(&output)
// Execute the command
err := cmd.ExecuteContext(t.Context())
require.NoError(t, err)
// Verify the project was updated correctly
expected := []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: "example-deployment"},
}
assert.Equal(t, expected, mockProject.Spec.ClusterResourceWhitelist)
// Verify the output
assert.Contains(t, output.String(), "Group 'apps', kind 'Deployment', and name 'example-deployment' is added to allowed cluster resources")
}
func Test_modifyNamespacedResourceList(t *testing.T) {
tests := []struct {
name string
initialList []metav1.GroupKind
add bool
group string
kind string
expectedList []metav1.GroupKind
expectedResult bool
}{
{
name: "Add new item to empty list",
initialList: []metav1.GroupKind{},
add: true,
group: "apps",
kind: "Deployment",
expectedList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
expectedResult: true,
},
{
name: "Add duplicate item",
initialList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
add: true,
group: "apps",
kind: "Deployment",
expectedList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
expectedResult: false,
},
{
name: "Remove existing item",
initialList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
add: false,
group: "apps",
kind: "Deployment",
expectedList: []metav1.GroupKind{},
expectedResult: true,
},
{
name: "Remove non-existent item",
initialList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
add: false,
group: "apps",
kind: "StatefulSet",
expectedList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
expectedResult: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
list := tt.initialList
result, _ := modifyNamespacedResourcesList(&list, tt.add, "", tt.group, tt.kind)
assert.Equal(t, tt.expectedResult, result)
assert.Equal(t, tt.expectedList, list)
})
}
}
func Test_modifyAllowClusterResourceList(t *testing.T) {
tests := []struct {
name string
initialList []v1alpha1.ClusterResourceRestrictionItem
add bool
group string
kind string
resourceName string
expectedList []v1alpha1.ClusterResourceRestrictionItem
expectedResult bool
}{
{
name: "Add new item to empty list",
initialList: []v1alpha1.ClusterResourceRestrictionItem{},
add: true,
group: "apps",
kind: "Deployment",
resourceName: "",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
expectedResult: true,
},
{
name: "Add duplicate item",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
add: true,
group: "apps",
kind: "Deployment",
resourceName: "",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
expectedResult: false,
},
{
name: "Remove existing item",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
add: false,
group: "apps",
kind: "Deployment",
resourceName: "",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{},
expectedResult: true,
},
{
name: "Remove non-existent item",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
add: false,
group: "apps",
kind: "StatefulSet",
resourceName: "",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
expectedResult: false,
},
{
name: "Add item with name",
initialList: []v1alpha1.ClusterResourceRestrictionItem{},
add: true,
group: "apps",
kind: "Deployment",
resourceName: "example-deployment",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: "example-deployment"},
},
expectedResult: true,
},
{
name: "Remove item with name",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: "example-deployment"},
},
add: false,
group: "apps",
kind: "Deployment",
resourceName: "example-deployment",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{},
expectedResult: true,
},
{
name: "Attempt to remove item with name but only group and kind exist",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
add: false,
group: "apps",
kind: "Deployment",
resourceName: "example-deployment",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
expectedResult: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
list := tt.initialList
result, _ := modifyClusterResourcesList(&list, tt.add, "", tt.group, tt.kind, tt.resourceName)
assert.Equal(t, tt.expectedResult, result)
assert.Equal(t, tt.expectedList, list)
})
}
}

View File

@@ -43,8 +43,8 @@ func AddProjFlags(command *cobra.Command, opts *ProjectOpts) {
command.Flags().StringSliceVar(&opts.SignatureKeys, "signature-keys", []string{}, "GnuPG public key IDs for commit signature verification")
command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring")
command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should have a warning condition when orphaned resources detected")
command.Flags().StringArrayVar(&opts.allowedClusterResources, "allow-cluster-resource", []string{}, "List of allowed cluster level resources")
command.Flags().StringArrayVar(&opts.deniedClusterResources, "deny-cluster-resource", []string{}, "List of denied cluster level resources")
command.Flags().StringArrayVar(&opts.allowedClusterResources, "allow-cluster-resource", []string{}, "List of allowed cluster level resources, optionally with group and name (e.g. ClusterRole, apiextensions.k8s.io/CustomResourceDefinition, /Namespace/team1-*)")
command.Flags().StringArrayVar(&opts.deniedClusterResources, "deny-cluster-resource", []string{}, "List of denied cluster level resources, optionally with group and name (e.g. ClusterRole, apiextensions.k8s.io/CustomResourceDefinition, /Namespace/kube-*)")
command.Flags().StringArrayVar(&opts.allowedNamespacedResources, "allow-namespaced-resource", []string{}, "List of allowed namespaced resources")
command.Flags().StringArrayVar(&opts.deniedNamespacedResources, "deny-namespaced-resource", []string{}, "List of denied namespaced resources")
command.Flags().StringSliceVar(&opts.SourceNamespaces, "source-namespaces", []string{}, "List of source namespaces for applications")
@@ -64,12 +64,26 @@ func getGroupKindList(values []string) []metav1.GroupKind {
return res
}
func (opts *ProjectOpts) GetAllowedClusterResources() []metav1.GroupKind {
return getGroupKindList(opts.allowedClusterResources)
func getClusterResourceRestrictionItemList(values []string) []v1alpha1.ClusterResourceRestrictionItem {
var res []v1alpha1.ClusterResourceRestrictionItem
for _, val := range values {
if parts := strings.Split(val, "/"); len(parts) == 3 {
res = append(res, v1alpha1.ClusterResourceRestrictionItem{Group: parts[0], Kind: parts[1], Name: parts[2]})
} else if parts = strings.Split(val, "/"); len(parts) == 2 {
res = append(res, v1alpha1.ClusterResourceRestrictionItem{Group: parts[0], Kind: parts[1]})
} else if len(parts) == 1 {
res = append(res, v1alpha1.ClusterResourceRestrictionItem{Kind: parts[0]})
}
}
return res
}
func (opts *ProjectOpts) GetDeniedClusterResources() []metav1.GroupKind {
return getGroupKindList(opts.deniedClusterResources)
func (opts *ProjectOpts) GetAllowedClusterResources() []v1alpha1.ClusterResourceRestrictionItem {
return getClusterResourceRestrictionItemList(opts.allowedClusterResources)
}
func (opts *ProjectOpts) GetDeniedClusterResources() []v1alpha1.ClusterResourceRestrictionItem {
return getClusterResourceRestrictionItemList(opts.deniedClusterResources)
}
func (opts *ProjectOpts) GetAllowedNamespacedResources() []metav1.GroupKind {

View File

@@ -19,8 +19,8 @@ func TestProjectOpts_ResourceLists(t *testing.T) {
assert.ElementsMatch(t, []metav1.GroupKind{{Kind: "ConfigMap"}}, opts.GetAllowedNamespacedResources())
assert.ElementsMatch(t, []metav1.GroupKind{{Group: "apps", Kind: "DaemonSet"}}, opts.GetDeniedNamespacedResources())
assert.ElementsMatch(t, []metav1.GroupKind{{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"}}, opts.GetAllowedClusterResources())
assert.ElementsMatch(t, []metav1.GroupKind{{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"}}, opts.GetDeniedClusterResources())
assert.ElementsMatch(t, []v1alpha1.ClusterResourceRestrictionItem{{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"}}, opts.GetAllowedClusterResources())
assert.ElementsMatch(t, []v1alpha1.ClusterResourceRestrictionItem{{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"}}, opts.GetDeniedClusterResources())
}
func TestProjectOpts_GetDestinationServiceAccounts(t *testing.T) {