package acl import ( "testing" "github.com/hashicorp/nomad/ci" "github.com/stretchr/testify/assert" ) func TestCapabilitySet(t *testing.T) { ci.Parallel(t) var cs capabilitySet = make(map[string]struct{}) // Check no capabilities by default if cs.Check(PolicyDeny) { t.Fatalf("unexpected check") } // Do a set and check cs.Set(PolicyDeny) if !cs.Check(PolicyDeny) { t.Fatalf("missing check") } // Clear and check cs.Clear() if cs.Check(PolicyDeny) { t.Fatalf("unexpected check") } } func TestMaxPrivilege(t *testing.T) { ci.Parallel(t) type tcase struct { Privilege string PrecedenceOver []string } tcases := []tcase{ { PolicyDeny, []string{PolicyDeny, PolicyWrite, PolicyRead, ""}, }, { PolicyWrite, []string{PolicyWrite, PolicyRead, ""}, }, { PolicyRead, []string{PolicyRead, ""}, }, } for idx1, tc := range tcases { for idx2, po := range tc.PrecedenceOver { if maxPrivilege(tc.Privilege, po) != tc.Privilege { t.Fatalf("failed %d %d", idx1, idx2) } if maxPrivilege(po, tc.Privilege) != tc.Privilege { t.Fatalf("failed %d %d", idx1, idx2) } } } } func TestACLManagement(t *testing.T) { ci.Parallel(t) assert := assert.New(t) // Create management ACL acl, err := NewACL(true, nil) assert.Nil(err) // Check default namespace rights assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs)) assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob)) assert.True(acl.AllowNamespace("default")) // Check non-specified namespace assert.True(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs)) assert.True(acl.AllowNamespace("foo")) // Check the other simpler operations assert.True(acl.IsManagement()) assert.True(acl.AllowAgentRead()) assert.True(acl.AllowAgentWrite()) assert.True(acl.AllowNodeRead()) assert.True(acl.AllowNodeWrite()) assert.True(acl.AllowOperatorRead()) assert.True(acl.AllowOperatorWrite()) assert.True(acl.AllowQuotaRead()) assert.True(acl.AllowQuotaWrite()) } func TestACLMerge(t *testing.T) { ci.Parallel(t) assert := assert.New(t) // Merge read + write policy p1, err := Parse(readAll) assert.Nil(err) p2, err := Parse(writeAll) assert.Nil(err) acl, err := NewACL(false, []*Policy{p1, p2}) assert.Nil(err) // Check default namespace rights assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs)) assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob)) assert.True(acl.AllowNamespace("default")) // Check non-specified namespace assert.False(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs)) assert.False(acl.AllowNamespace("foo")) // Check the other simpler operations assert.False(acl.IsManagement()) assert.True(acl.AllowAgentRead()) assert.True(acl.AllowAgentWrite()) assert.True(acl.AllowNodeRead()) assert.True(acl.AllowNodeWrite()) assert.True(acl.AllowOperatorRead()) assert.True(acl.AllowOperatorWrite()) assert.True(acl.AllowQuotaRead()) assert.True(acl.AllowQuotaWrite()) // Merge read + blank p3, err := Parse("") assert.Nil(err) acl, err = NewACL(false, []*Policy{p1, p3}) assert.Nil(err) // Check default namespace rights assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs)) assert.False(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob)) // Check non-specified namespace assert.False(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs)) // Check the other simpler operations assert.False(acl.IsManagement()) assert.True(acl.AllowAgentRead()) assert.False(acl.AllowAgentWrite()) assert.True(acl.AllowNodeRead()) assert.False(acl.AllowNodeWrite()) assert.True(acl.AllowOperatorRead()) assert.False(acl.AllowOperatorWrite()) assert.True(acl.AllowQuotaRead()) assert.False(acl.AllowQuotaWrite()) // Merge read + deny p4, err := Parse(denyAll) assert.Nil(err) acl, err = NewACL(false, []*Policy{p1, p4}) assert.Nil(err) // Check default namespace rights assert.False(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs)) assert.False(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob)) // Check non-specified namespace assert.False(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs)) // Check the other simpler operations assert.False(acl.IsManagement()) assert.False(acl.AllowAgentRead()) assert.False(acl.AllowAgentWrite()) assert.False(acl.AllowNodeRead()) assert.False(acl.AllowNodeWrite()) assert.False(acl.AllowOperatorRead()) assert.False(acl.AllowOperatorWrite()) assert.False(acl.AllowQuotaRead()) assert.False(acl.AllowQuotaWrite()) } var readAll = ` namespace "default" { policy = "read" } agent { policy = "read" } node { policy = "read" } operator { policy = "read" } quota { policy = "read" } ` var writeAll = ` namespace "default" { policy = "write" } agent { policy = "write" } node { policy = "write" } operator { policy = "write" } quota { policy = "write" } ` var denyAll = ` namespace "default" { policy = "deny" } agent { policy = "deny" } node { policy = "deny" } operator { policy = "deny" } quota { policy = "deny" } ` func TestAllowNamespace(t *testing.T) { ci.Parallel(t) tests := []struct { Policy string Allow bool }{ { Policy: `namespace "foo" {}`, Allow: false, }, { Policy: `namespace "foo" { policy = "deny" }`, Allow: false, }, { Policy: `namespace "foo" { capabilities = ["deny"] }`, Allow: false, }, { Policy: `namespace "foo" { capabilities = ["list-jobs"] }`, Allow: true, }, { Policy: `namespace "foo" { policy = "read" }`, Allow: true, }, } for _, tc := range tests { t.Run(tc.Policy, func(t *testing.T) { assert := assert.New(t) policy, err := Parse(tc.Policy) assert.Nil(err) acl, err := NewACL(false, []*Policy{policy}) assert.Nil(err) assert.Equal(tc.Allow, acl.AllowNamespace("foo")) }) } } func TestWildcardNamespaceMatching(t *testing.T) { ci.Parallel(t) tests := []struct { Policy string Allow bool }{ { // Wildcard matches Policy: `namespace "prod-api-*" { policy = "write" }`, Allow: true, }, { // Non globbed namespaces are not wildcards Policy: `namespace "prod-api" { policy = "write" }`, Allow: false, }, { // Concrete matches take precedence Policy: `namespace "prod-api-services" { policy = "deny" } namespace "prod-api-*" { policy = "write" }`, Allow: false, }, { Policy: `namespace "prod-api-*" { policy = "deny" } namespace "prod-api-services" { policy = "write" }`, Allow: true, }, { // The closest character match wins Policy: `namespace "*-api-services" { policy = "deny" } namespace "prod-api-*" { policy = "write" }`, // 4 vs 8 chars Allow: false, }, { Policy: `namespace "prod-api-*" { policy = "write" } namespace "*-api-services" { policy = "deny" }`, // 4 vs 8 chars Allow: false, }, } for _, tc := range tests { t.Run(tc.Policy, func(t *testing.T) { assert := assert.New(t) policy, err := Parse(tc.Policy) assert.NoError(err) assert.NotNil(policy.Namespaces) acl, err := NewACL(false, []*Policy{policy}) assert.Nil(err) assert.Equal(tc.Allow, acl.AllowNamespace("prod-api-services")) }) } } func TestWildcardHostVolumeMatching(t *testing.T) { ci.Parallel(t) tests := []struct { Policy string Allow bool }{ { // Wildcard matches Policy: `host_volume "prod-api-*" { policy = "write" }`, Allow: true, }, { // Non globbed volumes are not wildcards Policy: `host_volume "prod-api" { policy = "write" }`, Allow: false, }, { // Concrete matches take precedence Policy: `host_volume "prod-api-services" { policy = "deny" } host_volume "prod-api-*" { policy = "write" }`, Allow: false, }, { Policy: `host_volume "prod-api-*" { policy = "deny" } host_volume "prod-api-services" { policy = "write" }`, Allow: true, }, { // The closest character match wins Policy: `host_volume "*-api-services" { policy = "deny" } host_volume "prod-api-*" { policy = "write" }`, // 4 vs 8 chars Allow: false, }, { Policy: `host_volume "prod-api-*" { policy = "write" } host_volume "*-api-services" { policy = "deny" }`, // 4 vs 8 chars Allow: false, }, } for _, tc := range tests { t.Run(tc.Policy, func(t *testing.T) { assert := assert.New(t) policy, err := Parse(tc.Policy) assert.NoError(err) assert.NotNil(policy.HostVolumes) acl, err := NewACL(false, []*Policy{policy}) assert.Nil(err) assert.Equal(tc.Allow, acl.AllowHostVolume("prod-api-services")) }) } } func TestACL_matchingCapabilitySet_returnsAllMatches(t *testing.T) { ci.Parallel(t) tests := []struct { Policy string NS string MatchingGlobs []string }{ { Policy: `namespace "production-*" { policy = "write" }`, NS: "production-api", MatchingGlobs: []string{"production-*"}, }, { Policy: `namespace "prod-*" { policy = "write" }`, NS: "production-api", MatchingGlobs: nil, }, { Policy: `namespace "production-*" { policy = "write" } namespace "production-*-api" { policy = "deny" }`, NS: "production-admin-api", MatchingGlobs: []string{"production-*", "production-*-api"}, }, } for _, tc := range tests { t.Run(tc.Policy, func(t *testing.T) { assert := assert.New(t) policy, err := Parse(tc.Policy) assert.NoError(err) assert.NotNil(policy.Namespaces) acl, err := NewACL(false, []*Policy{policy}) assert.Nil(err) var namespaces []string for _, cs := range findAllMatchingWildcards(acl.wildcardNamespaces, tc.NS) { namespaces = append(namespaces, cs.name) } assert.Equal(tc.MatchingGlobs, namespaces) }) } } func TestACL_matchingCapabilitySet_difference(t *testing.T) { ci.Parallel(t) tests := []struct { Policy string NS string Difference int }{ { Policy: `namespace "production-*" { policy = "write" }`, NS: "production-api", Difference: 3, }, { Policy: `namespace "production-*" { policy = "write" }`, NS: "production-admin-api", Difference: 9, }, { Policy: `namespace "production-**" { policy = "write" }`, NS: "production-admin-api", Difference: 9, }, { Policy: `namespace "*" { policy = "write" }`, NS: "production-admin-api", Difference: 20, }, { Policy: `namespace "*admin*" { policy = "write" }`, NS: "production-admin-api", Difference: 15, }, } for _, tc := range tests { t.Run(tc.Policy, func(t *testing.T) { assert := assert.New(t) policy, err := Parse(tc.Policy) assert.NoError(err) assert.NotNil(policy.Namespaces) acl, err := NewACL(false, []*Policy{policy}) assert.Nil(err) matches := findAllMatchingWildcards(acl.wildcardNamespaces, tc.NS) assert.Equal(tc.Difference, matches[0].difference) }) } }