diff --git a/go.mod b/go.mod index e11ee690..cdc92c2e 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b // indirect github.com/kylelemons/godebug v1.1.0 github.com/leodido/go-urn v1.2.0 // indirect + github.com/maxatome/go-testdeep v1.9.2 // indirect github.com/pkg/errors v0.9.1 github.com/pmylund/go-cache v2.1.0+incompatible github.com/sirupsen/logrus v1.7.0 diff --git a/go.sum b/go.sum index 7e3c4df2..307fd83b 100644 --- a/go.sum +++ b/go.sum @@ -664,6 +664,8 @@ github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxatome/go-testdeep v1.9.2 h1:5D7u/JkeG0A/HDTbZ/CFFmpYZPeWN+uGQ1IbsEHYlCo= +github.com/maxatome/go-testdeep v1.9.2/go.mod h1:011SgQ6efzZYAen6fDn4BqQ+lUR72ysdyKe7Dyogw70= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE= github.com/mholt/archiver/v3 v3.5.0/go.mod h1:qqTTPUK/HZPFgFQ/TJ3BzvTpF/dPtFVJXdQbCmeMxwc= diff --git a/pkg/permissions/generate.go b/pkg/permissions/generate.go index 1515fc8a..61711ea3 100644 --- a/pkg/permissions/generate.go +++ b/pkg/permissions/generate.go @@ -2,7 +2,6 @@ package permissions import ( "fmt" - "strings" "github.com/jetstack/preflight/pkg/agent" "github.com/jetstack/preflight/pkg/datagatherer/k8s" @@ -10,66 +9,107 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func Generate(dataGatherers []agent.DataGatherer) string { - var accumulator string = "" - - for _, g := range dataGatherers { - if g.Kind != "k8s-dynamic" { - continue - } - - genericConfig := g.Config - dyConfig := genericConfig.(*k8s.ConfigDynamic) - - metaName := fmt.Sprint(dyConfig.GroupVersionResource.Resource) - - accumulator = fmt.Sprintf(`%s -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: jetstack-secure-agent-%s-reader -rules: -- apiGroups: ["%s"] - resources: ["%s"] - verbs: ["get", "list", "watch"] ----`, accumulator, metaName, dyConfig.GroupVersionResource.Group, dyConfig.GroupVersionResource.Resource) - } - - s := strings.TrimPrefix(accumulator, "\n") - ss := strings.TrimSuffix(s, "---") - return strings.TrimSuffix(ss, "\n") +// AgentRBACManifests is a wrapper around the various RBAC structs needed to grant the agent fine-grained permissions as per its dg configs +type AgentRBACManifests struct { + // ClusterRoles is a list of roles for resources the agent will collect + ClusterRoles []rbac.ClusterRole + // ClusterRoleBindings is a list of crbs for resources which have no include/exclude ns configured + ClusterRoleBindings []rbac.ClusterRoleBinding + // RoleBindings is a list of namespaced bindings to grant permissions when include/exclude ns set + RoleBindings []rbac.RoleBinding } -func GenerateRoles(dataGatherer []agent.DataGatherer) []rbac.ClusterRole { - out := []rbac.ClusterRole{} +const agentNamespace = "jetstack-secure" +const agentSubjectName = "agent" - for _, g := range dataGatherer { - if g.Kind != "k8s-dynamic" { +func GenerateAgentRBACManifests(dataGatherers []agent.DataGatherer) AgentRBACManifests { + // create a new AgentRBACManifest struct + var AgentRBACManifests AgentRBACManifests + + for _, dg := range dataGatherers { + if dg.Kind != "k8s-dynamic" { continue } - genericConfig := g.Config - dyConfig := genericConfig.(*k8s.ConfigDynamic) - - metaName := dyConfig.GroupVersionResource.Resource + dyConfig := dg.Config.(*k8s.ConfigDynamic) + metadataName := fmt.Sprintf("%s-agent-%s-reader", agentNamespace, dyConfig.GroupVersionResource.Resource) - out = append(out, rbac.ClusterRole{ + AgentRBACManifests.ClusterRoles = append(AgentRBACManifests.ClusterRoles, rbac.ClusterRole{ TypeMeta: metav1.TypeMeta{ Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("jetstack-secure-agent-%s-reader", metaName), + Name: metadataName, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get", "list", "watch"}, APIGroups: []string{dyConfig.GroupVersionResource.Group}, - Resources: []string{metaName}, + Resources: []string{dyConfig.GroupVersionResource.Resource}, }, }, }) + // if dyConfig.IncludeNamespaces has more than 0 items in it + // then, for each namespace create a rbac.RoleBinding in that namespace + if len(dyConfig.IncludeNamespaces) != 0 { + for _, ns := range dyConfig.IncludeNamespaces { + AgentRBACManifests.RoleBindings = append(AgentRBACManifests.RoleBindings, rbac.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + + ObjectMeta: metav1.ObjectMeta{ + Name: metadataName, + Namespace: ns, + }, + + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + Name: agentSubjectName, + Namespace: agentNamespace, + }, + }, + + RoleRef: rbac.RoleRef{ + Kind: "ClusterRole", + Name: metadataName, + APIGroup: "rbac.authorization.k8s.io", + }, + }) + } + } else { + // only do this if the dg does not have IncludeNamespaces set + AgentRBACManifests.ClusterRoleBindings = append(AgentRBACManifests.ClusterRoleBindings, rbac.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + + ObjectMeta: metav1.ObjectMeta{ + Name: metadataName, + }, + + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + Name: agentSubjectName, + Namespace: agentNamespace, + }, + }, + + RoleRef: rbac.RoleRef{ + Kind: "ClusterRole", + Name: metadataName, + APIGroup: "rbac.authorization.k8s.io", + }, + }) + } + } - return out + + return AgentRBACManifests } diff --git a/pkg/permissions/generate_test.go b/pkg/permissions/generate_test.go index 6428e9e2..8efbc6de 100644 --- a/pkg/permissions/generate_test.go +++ b/pkg/permissions/generate_test.go @@ -3,24 +3,22 @@ package permissions import ( "testing" - "github.com/d4l3k/messagediff" "github.com/jetstack/preflight/pkg/agent" "github.com/jetstack/preflight/pkg/datagatherer/k8s" + "github.com/maxatome/go-testdeep/td" rbac "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) -func TestGenerateRBAC(t *testing.T) { - // Use these test cases to check if Generate function is correct +func TestGenerateAgentRBACManifests(t *testing.T) { testCases := []struct { - // expectedClusterRoles is the collection of ClusterRole - expectedClusterRoles []rbac.ClusterRole - dataGatherers []agent.DataGatherer - description string + description string + dataGatherers []agent.DataGatherer + expectedAgentRBACManifests AgentRBACManifests }{ { - description: "Generate RBAC struct for pods datagatherer", + description: "Generate ClusterRole and ClusterRoleBinding for simple pod dg use case", dataGatherers: []agent.DataGatherer{ { Name: "k8s/pods", @@ -32,87 +30,138 @@ func TestGenerateRBAC(t *testing.T) { }, }, }, - { - Name: "k8s/secrets", - Kind: "k8s-dynamic", - Config: &k8s.ConfigDynamic{ - GroupVersionResource: schema.GroupVersionResource{ - Version: "v1", - Resource: "secrets", + }, + expectedAgentRBACManifests: AgentRBACManifests{ + ClusterRoles: []rbac.ClusterRole{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "jetstack-secure-agent-pods-reader", + }, + Rules: []rbac.PolicyRule{ + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{""}, + Resources: []string{"pods"}, + }, + }, + }, + }, + ClusterRoleBindings: []rbac.ClusterRoleBinding{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "jetstack-secure-agent-pods-reader", + }, + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + Name: "agent", + Namespace: "jetstack-secure", + }, + }, + RoleRef: rbac.RoleRef{ + Kind: "ClusterRole", + Name: "jetstack-secure-agent-pods-reader", + APIGroup: "rbac.authorization.k8s.io", }, }, }, + }, + }, + { + description: "Generate RBAC config for simple pod dg use case where only two namespace are included", + dataGatherers: []agent.DataGatherer{ { - Name: "k8s/awspcaissuer", + Name: "k8s/pods", Kind: "k8s-dynamic", Config: &k8s.ConfigDynamic{ GroupVersionResource: schema.GroupVersionResource{ - Group: "awspca.cert-manager.io", Version: "v1", - Resource: "awspcaissuers", + Resource: "pods", }, + IncludeNamespaces: []string{"example", "foobar"}, }, }, }, - expectedClusterRoles: []rbac.ClusterRole{ - { - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterRole", - APIVersion: "rbac.authorization.k8s.io/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "jetstack-secure-agent-pods-reader", - }, - Rules: []rbac.PolicyRule{ - { - Verbs: []string{"get", "list", "watch"}, - APIGroups: []string{""}, - Resources: []string{"pods"}, + expectedAgentRBACManifests: AgentRBACManifests{ + ClusterRoles: []rbac.ClusterRole{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: "rbac.authorization.k8s.io/v1", }, - }, - }, - { - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterRole", - APIVersion: "rbac.authorization.k8s.io/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "jetstack-secure-agent-secrets-reader", - }, - Rules: []rbac.PolicyRule{ - { - Verbs: []string{"get", "list", "watch"}, - APIGroups: []string{""}, - Resources: []string{"secrets"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "jetstack-secure-agent-pods-reader", + }, + Rules: []rbac.PolicyRule{ + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{""}, + Resources: []string{"pods"}, + }, }, }, }, - { - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterRole", - APIVersion: "rbac.authorization.k8s.io/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "jetstack-secure-agent-awspcaissuers-reader", + RoleBindings: []rbac.RoleBinding{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "jetstack-secure-agent-pods-reader", + Namespace: "example", + }, + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + Name: "agent", + Namespace: "jetstack-secure", + }, + }, + RoleRef: rbac.RoleRef{ + Kind: "ClusterRole", + Name: "jetstack-secure-agent-pods-reader", + APIGroup: "rbac.authorization.k8s.io", + }, }, - Rules: []rbac.PolicyRule{ - { - Verbs: []string{"get", "list", "watch"}, - APIGroups: []string{"awspca.cert-manager.io"}, - Resources: []string{"awspcaissuers"}, + { + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "jetstack-secure-agent-pods-reader", + Namespace: "foobar", + }, + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + Name: "agent", + Namespace: "jetstack-secure", + }, + }, + RoleRef: rbac.RoleRef{ + Kind: "ClusterRole", + Name: "jetstack-secure-agent-pods-reader", + APIGroup: "rbac.authorization.k8s.io", }, }, }, }, }, - // Try adding more test cases } for _, input := range testCases { - got := GenerateRoles(input.dataGatherers) - if diff, equal := messagediff.PrettyDiff(input.expectedClusterRoles, got); !equal { - t.Errorf("%s:\n%s", input.description, diff) - t.Fatalf("unexpected difference in RBAC cluster role: \ngot \n%v\nwant\n%v", got, input.expectedClusterRoles) - } + got := GenerateAgentRBACManifests(input.dataGatherers) + + td.Cmp(t, input.expectedAgentRBACManifests, got) } }