diff --git a/feature/gnsi/authz/tests/authz/README.md b/feature/gnsi/authz/tests/authz/README.md index 6a5aebd99c0..5f9c2dfb9ee 100644 --- a/feature/gnsi/authz/tests/authz/README.md +++ b/feature/gnsi/authz/tests/authz/README.md @@ -1,4 +1,4 @@ -# Authz: General Authz (1-4) tests +# Authz: General Authz tests ## Summary @@ -347,8 +347,12 @@ For each of the scenarios in this section, we need to exercise the following 3 a 1. Use `gNSI.Rotate` method to push and finalize policy `policy-normal-1`, with `create_on` = `100` and `version` = `policy-normal-1_v1`. 2. Ensure all results match per the above table for policy `policy-normal-1`. -* TODO: Authz-1.5, "Test principle prefix and suffix match" - * Test the behavior of [prefix and suffix match on principles](https://github.com/grpc/proposal/blob/eb0d8fcc93820d3039ac851f8a36bdf2554cab6a/A43-grpc-authorization-api.md?plain=1#L73-L74) +* Authz-1.5, "Test principal prefix and suffix match" + 1. Use `gNSI.Rotate` method to push and finalize policy `policy-prefix-suffix-match`, with `create_on` = `100` and `version` = `policy-prefix-suffix-match_v1`. + 2. Ensure all results match per the following: + * All users matching `spiffe://test-abc.foo.bar/xyz/*` (prefix match) are allowed to issue `gNMI.Get` method. + * Only users matching `*admin` (suffix match, e.g., `cert_user_admin`) are allowed to issue `gRIBI.Get` method. + * Other users are denied to issue `gRIBI.Get` method. ### Authz-2, test rotation behavior diff --git a/feature/gnsi/authz/tests/authz/authz1_4_test.go b/feature/gnsi/authz/tests/authz/authz_test.go similarity index 63% rename from feature/gnsi/authz/tests/authz/authz1_4_test.go rename to feature/gnsi/authz/tests/authz/authz_test.go index c919c9cc2c5..3023a0d2081 100644 --- a/feature/gnsi/authz/tests/authz/authz1_4_test.go +++ b/feature/gnsi/authz/tests/authz/authz_test.go @@ -20,6 +20,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/json" "flag" "fmt" "os" @@ -36,6 +37,7 @@ import ( "github.com/openconfig/ondatra" "github.com/openconfig/ondatra/gnmi" "github.com/openconfig/testt" + "github.com/openconfig/ygnmi/ygnmi" ) const ( @@ -43,6 +45,81 @@ const ( maxCompWaitTime = 600 ) +// Verify wraps Verify and adds OpenConfig telemetry validation for RPC counters and timestamps. +func Verify(t testing.TB, dut *ondatra.DUTDevice, spiffe *authz.Spiffe, rpc *gnxi.RPC, opts ...authz.VerifyOpt) { + expectedRes := authzpb.ProbeResponse_ACTION_PERMIT + hardVerify := false + for _, opt := range opts { + switch opt.(type) { + case *authz.ExceptDeny: + expectedRes = authzpb.ProbeResponse_ACTION_DENY + case *authz.HardVerify: + hardVerify = true + } + } + + serverName := "DEFAULT" + var counterPath ygnmi.SingletonQuery[uint64] + var timePath ygnmi.SingletonQuery[uint64] + namePath := gnmi.OC().System().GrpcServer(serverName).AuthzPolicyCounters().Rpc(rpc.Path).Name().State() + if expectedRes == authzpb.ProbeResponse_ACTION_PERMIT { + counterPath = gnmi.OC().System().GrpcServer(serverName).AuthzPolicyCounters().Rpc(rpc.Path).AccessAccepts().State() + timePath = gnmi.OC().System().GrpcServer(serverName).AuthzPolicyCounters().Rpc(rpc.Path).LastAccessAccept().State() + } else { + counterPath = gnmi.OC().System().GrpcServer(serverName).AuthzPolicyCounters().Rpc(rpc.Path).AccessRejects().State() + timePath = gnmi.OC().System().GrpcServer(serverName).AuthzPolicyCounters().Rpc(rpc.Path).LastAccessReject().State() + } + + getVal := func(path ygnmi.SingletonQuery[uint64]) uint64 { + val, ok := gnmi.Lookup(t, dut, path).Val() + if !ok { + return 0 + } + return val + } + + var valBefore, timeBefore uint64 + if hardVerify { + valBefore = getVal(counterPath) + timeBefore = getVal(timePath) + } + + // Call the library Verify + authz.Verify(t, dut, spiffe, rpc, opts...) + + if hardVerify { + valAfter := getVal(counterPath) + timeAfter := getVal(timePath) + + if valAfter != valBefore+1 { + t.Errorf("Expected counter %s to increment from %d to %d, got %d", rpc.Path, valBefore, valBefore+1, valAfter) + } + if timeAfter < timeBefore { + t.Errorf("Expected timestamp %s to be >= %d, got %d", rpc.Path, timeBefore, timeAfter) + } + if timeAfter == 0 { + t.Errorf("Expected timestamp %s to be > 0 after call", rpc.Path) + } + nameVal, ok := gnmi.Lookup(t, dut, namePath).Val() + if !ok || nameVal != rpc.Path { + t.Errorf("Expected RPC name in telemetry to be %s, got %s (ok: %t)", rpc.Path, nameVal, ok) + } + } +} + +// verifyPolicyMetadata validates the active policy version and creation time in telemetry. +func verifyPolicyMetadata(t testing.TB, dut *ondatra.DUTDevice, expVersion string, expCreatedOn uint64) { + serverName := "DEFAULT" + versionTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyVersion().State()) + createdOnTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyCreatedOn().State()) + if versionTele != expVersion { + t.Errorf("Expected telemetry version %s, got %s", expVersion, versionTele) + } + if createdOnTele != expCreatedOn { + t.Errorf("Expected telemetry createdOn %d, got %d", expCreatedOn, createdOnTele) + } +} + type UsersMap map[string]authz.Spiffe var ( @@ -179,11 +256,20 @@ func setUpBaseline(t *testing.T, dut *ondatra.DUTDevice) { func verifyAuthTable(t *testing.T, dut *ondatra.DUTDevice, authTable authorizationTable) { for certName, access := range authTable { t.Run(fmt.Sprintf("Validating access for user %s", certName), func(t *testing.T) { + // Probe only after finalize + for _, allowedRPC := range access.allowed { + Verify(t, dut, getSpiffe(t, dut, certName), allowedRPC) + } + for _, deniedRPC := range access.denied { + Verify(t, dut, getSpiffe(t, dut, certName), deniedRPC, &authz.ExceptDeny{}) + } + + // Actual calls after finalize for _, allowedRPC := range access.allowed { - authz.Verify(t, dut, getSpiffe(t, dut, certName), allowedRPC, &authz.HardVerify{}) + Verify(t, dut, getSpiffe(t, dut, certName), allowedRPC, &authz.HardVerify{}) } for _, deniedRPC := range access.denied { - authz.Verify(t, dut, getSpiffe(t, dut, certName), deniedRPC, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, getSpiffe(t, dut, certName), deniedRPC, &authz.ExceptDeny{}, &authz.HardVerify{}) } }) } @@ -197,6 +283,7 @@ func getSpiffe(t *testing.T, dut *ondatra.DUTDevice, certName string) *authz.Spi return &spiffe } +// Authz-1, Test policy behaviors, and probe results matches actual client results. // Authz-1, Test policy behaviors, and probe results matches actual client results. func TestAuthz1(t *testing.T) { dut := ondatra.DUT(t, "dut") @@ -222,12 +309,22 @@ func TestAuthz1(t *testing.T) { newpolicy.AddAllowRules("base", []string{*testInfraID}, []*gnxi.RPC{gnxi.RPCs.GnsiAuthzAllRPC}) // Rotate the policy. - newpolicy.Rotate(t, dut, uint64(100), "policy-everyone-can-gnmi-not-gribi_v1", false) + newpolicy.RotateWithOptions(t, dut, uint64(100), "policy-everyone-can-gnmi-not-gribi_v1", false, func() { + t.Run("Verification of Policy BEFORE finalize (Probe only)", func(t *testing.T) { + Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}) + Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GnmiGet) + }) + }) + verifyPolicyMetadata(t, dut, "policy-everyone-can-gnmi-not-gribi_v1", uint64(100)) // Verification of Policy for cert_user_admin is allowed gNMI Get and denied gRIBI Get - t.Run("Verification of Policy for cert_user_admin is allowed gNMI Get and denied gRIBI Get", func(t *testing.T) { - authz.Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) - authz.Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) + t.Run("Verification of Policy for cert_user_admin is allowed gNMI Get and denied gRIBI Get (Probe after finalize)", func(t *testing.T) { + Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}) + Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GnmiGet) + }) + t.Run("Verification of Policy for cert_user_admin is allowed gNMI Get and denied gRIBI Get (Actual calls after finalize)", func(t *testing.T) { + Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) }) }) @@ -245,11 +342,21 @@ func TestAuthz1(t *testing.T) { } newpolicy.AddAllowRules("base", []string{*testInfraID}, []*gnxi.RPC{gnxi.RPCs.AllRPC}) // Rotate the policy. - newpolicy.Rotate(t, dut, uint64(100), "policy-everyone-can-gribi-not-gnmi_v1", false) + newpolicy.RotateWithOptions(t, dut, uint64(100), "policy-everyone-can-gribi-not-gnmi_v1", false, func() { + t.Run("Verification of Policy BEFORE finalize (Probe only)", func(t *testing.T) { + Verify(t, dut, getSpiffe(t, dut, "cert_deny_all"), gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}) + Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GribiGet) + }) + }) + verifyPolicyMetadata(t, dut, "policy-everyone-can-gribi-not-gnmi_v1", uint64(100)) - t.Run("Verification of cert_deny_all is denied to issue gRIBI.Get and cert_user_admin is allowed to issue `gRIBI.Get`", func(t *testing.T) { - authz.Verify(t, dut, getSpiffe(t, dut, "cert_deny_all"), gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) - authz.Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GribiGet, &authz.HardVerify{}) + t.Run("Verification of cert_deny_all is denied to issue gRIBI.Get and cert_user_admin is allowed to issue `gRIBI.Get` (Probe after finalize)", func(t *testing.T) { + Verify(t, dut, getSpiffe(t, dut, "cert_deny_all"), gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}) + Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GribiGet) + }) + t.Run("Verification of cert_deny_all is denied to issue gRIBI.Get and cert_user_admin is allowed to issue `gRIBI.Get` (Actual calls after finalize)", func(t *testing.T) { + Verify(t, dut, getSpiffe(t, dut, "cert_deny_all"), gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, certAdminSpiffe, gnxi.RPCs.GribiGet, &authz.HardVerify{}) }) }) @@ -268,11 +375,21 @@ func TestAuthz1(t *testing.T) { } newpolicy.AddAllowRules("base", []string{*testInfraID}, []*gnxi.RPC{gnxi.RPCs.AllRPC}) // Rotate the policy. - newpolicy.Rotate(t, dut, uint64(100), "policy-gribi-get_v1", false) + newpolicy.RotateWithOptions(t, dut, uint64(100), "policy-gribi-get_v1", false, func() { + t.Run("Verification of Policy BEFORE finalize (Probe only) - 1", func(t *testing.T) { + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GribiGet) + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}) + }) + }) + verifyPolicyMetadata(t, dut, "policy-gribi-get_v1", uint64(100)) - // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get - authz.Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GribiGet, &authz.HardVerify{}) - authz.Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get (Probe after finalize - 1) + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GribiGet) + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}) + + // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get (Actual calls after finalize - 1) + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GribiGet, &authz.HardVerify{}) + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) // Fetch the Desired Authorization Policy and Attach base Admin Policy Before Rotate - 2 newpolicy, ok = policyMap["policy-gnmi-get"] @@ -281,12 +398,25 @@ func TestAuthz1(t *testing.T) { } newpolicy.AddAllowRules("base", []string{*testInfraID}, []*gnxi.RPC{gnxi.RPCs.AllRPC}) // Rotate the policy. - newpolicy.Rotate(t, dut, uint64(time.Now().Unix()), fmt.Sprintf("v0.%v", (time.Now().UnixNano())), false) + expVersion := fmt.Sprintf("v0.%v", (time.Now().UnixNano())) + expCreatedOn := uint64(time.Now().Unix()) + newpolicy.RotateWithOptions(t, dut, expCreatedOn, expVersion, false, func() { + t.Run("Verification of Policy BEFORE finalize (Probe only) - 2", func(t *testing.T) { + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}) + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GnmiGet) + }) + }) + verifyPolicyMetadata(t, dut, expVersion, expCreatedOn) - // Verification of Policy for read-only to deny gRIBI Get and allow gNMI Get - t.Run("Verification of Policy for read-only to deny gRIBI Get and allow gNMI Get", func(t *testing.T) { - authz.Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) - authz.Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) + // Verification of Policy for read-only to deny gRIBI Get and allow gNMI Get (Probe after finalize - 2) + t.Run("Verification of Policy for read-only to deny gRIBI Get and allow gNMI Get (Probe after finalize)", func(t *testing.T) { + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}) + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GnmiGet) + }) + // Verification of Policy for read-only to deny gRIBI Get and allow gNMI Get (Actual calls after finalize - 2) + t.Run("Verification of Policy for read-only to deny gRIBI Get and allow gNMI Get (Actual calls after finalize)", func(t *testing.T) { + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, readOnlySpiffe, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) }) }) @@ -309,11 +439,85 @@ func TestAuthz1(t *testing.T) { } newpolicy.AddAllowRules("base", []string{*testInfraID}, []*gnxi.RPC{gnxi.RPCs.AllRPC}) // Rotate the policy. - newpolicy.Rotate(t, dut, uint64(100), "policy-normal-1_v1", false) + newpolicy.RotateWithOptions(t, dut, uint64(100), "policy-normal-1_v1", false, func() { + t.Run("Verification of Policy BEFORE finalize (Probe only)", func(t *testing.T) { + for certName, access := range authTable { + for _, allowedRPC := range access.allowed { + Verify(t, dut, getSpiffe(t, dut, certName), allowedRPC) + } + for _, deniedRPC := range access.denied { + Verify(t, dut, getSpiffe(t, dut, certName), deniedRPC, &authz.ExceptDeny{}) + } + } + }) + }) + verifyPolicyMetadata(t, dut, "policy-normal-1_v1", uint64(100)) // Verify all results match per the above table for policy policy-normal-1 verifyAuthTable(t, dut, authTable) }) + + t.Run("Authz-1.5, Test principal prefix and suffix match", func(t *testing.T) { + // Pre-Test Section + _, policyBefore := authz.Get(t, dut) + t.Logf("Authz Policy of the Device %s before the Rotate Trigger is %s", dut.Name(), policyBefore.PrettyPrint(t)) + defer policyBefore.Rotate(t, dut, uint64(time.Now().Unix()), fmt.Sprintf("v0.%v", (time.Now().UnixNano())), false) + + // Fetch the Desired Authorization Policy and Attach base Admin Policy Before Rotate + newpolicy, ok := policyMap["policy-prefix-suffix-match"] + if !ok { + t.Fatal("Policy policy-prefix-suffix-match is not loaded from policy json file") + } + newpolicy.AddAllowRules("base", []string{*testInfraID}, []*gnxi.RPC{gnxi.RPCs.AllRPC}) + // Rotate the policy. + newpolicy.RotateWithOptions(t, dut, uint64(100), "policy-prefix-suffix-match_v1", false, func() { + t.Run("Verification of Policy BEFORE finalize (Probe only)", func(t *testing.T) { + for certName, spiffe := range usersMap { + Verify(t, dut, &spiffe, gnxi.RPCs.GnmiGet) + if certName == "cert_user_admin" { + Verify(t, dut, &spiffe, gnxi.RPCs.GribiGet) + } else { + Verify(t, dut, &spiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}) + } + } + }) + }) + verifyPolicyMetadata(t, dut, "policy-prefix-suffix-match_v1", uint64(100)) + + // Verification (Probe after finalize) + for certName, spiffe := range usersMap { + t.Run(fmt.Sprintf("Verification of gNMI Get for %s (prefix match, Probe after finalize)", certName), func(t *testing.T) { + Verify(t, dut, &spiffe, gnxi.RPCs.GnmiGet) + }) + + if certName == "cert_user_admin" { + t.Run("Verification of gRIBI Get for admin (suffix match, Probe after finalize)", func(t *testing.T) { + Verify(t, dut, &spiffe, gnxi.RPCs.GribiGet) + }) + } else { + t.Run(fmt.Sprintf("Verification of gRIBI Get for %s (should be denied, Probe after finalize)", certName), func(t *testing.T) { + Verify(t, dut, &spiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}) + }) + } + } + + // Verification (Actual calls after finalize) + for certName, spiffe := range usersMap { + t.Run(fmt.Sprintf("Verification of gNMI Get for %s (prefix match, Actual calls after finalize)", certName), func(t *testing.T) { + Verify(t, dut, &spiffe, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) + }) + + if certName == "cert_user_admin" { + t.Run("Verification of gRIBI Get for admin (suffix match, Actual calls after finalize)", func(t *testing.T) { + Verify(t, dut, &spiffe, gnxi.RPCs.GribiGet, &authz.HardVerify{}) + }) + } else { + t.Run(fmt.Sprintf("Verification of gRIBI Get for %s (should be denied, Actual calls after finalize)", certName), func(t *testing.T) { + Verify(t, dut, &spiffe, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + }) + } + } + }) } // Authz-2, Test rotation behavior @@ -393,8 +597,8 @@ func TestAuthz2(t *testing.T) { } t.Run("Verification of Policy for user_admin to deny gRIBI Get and allow gNMI Get", func(t *testing.T) { // Verification of Policy for user_admin to deny gRIBI Get and allow gNMI Get - authz.Verify(t, dut, spiffeUserAdmin, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) - authz.Verify(t, dut, spiffeUserAdmin, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, spiffeUserAdmin, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) + Verify(t, dut, spiffeUserAdmin, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) }) }) @@ -412,12 +616,14 @@ func TestAuthz2(t *testing.T) { } newpolicy.AddAllowRules("base", []string{*testInfraID}, []*gnxi.RPC{gnxi.RPCs.AllRPC}) // Rotate the policy. - newpolicy.Rotate(t, dut, uint64(time.Now().Unix()), fmt.Sprintf("v0.%v", (time.Now().UnixNano())), false) + expVersion := fmt.Sprintf("v0.%v", (time.Now().UnixNano())) + expCreatedOn := uint64(time.Now().Unix()) + newpolicy.Rotate(t, dut, expCreatedOn, expVersion, false) // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get t.Run("Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get", func(t *testing.T) { - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) }) // Fetch the Desired Authorization Policy and Attach base Admin Policy Before Rotate @@ -453,17 +659,28 @@ func TestAuthz2(t *testing.T) { } // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get t.Run("Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get after rotate that is not finalized", func(t *testing.T) { - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) }) // Close the Stream rotateStream.CloseSend() + // Verify telemetry rolled back + serverName := "DEFAULT" + versionTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyVersion().State()) + createdOnTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyCreatedOn().State()) + if versionTele != expVersion { + t.Errorf("Expected rolled back telemetry version %s, got %s", expVersion, versionTele) + } + if createdOnTele != expCreatedOn { + t.Errorf("Expected rolled back telemetry createdOn %d, got %d", expCreatedOn, createdOnTele) + } + // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get t.Run("Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get after closing stream", func(t *testing.T) { - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) }) }) @@ -481,12 +698,14 @@ func TestAuthz2(t *testing.T) { } newpolicy.AddAllowRules("base", []string{*testInfraID}, []*gnxi.RPC{gnxi.RPCs.AllRPC}) // Rotate the policy. - newpolicy.Rotate(t, dut, uint64(time.Now().Unix()), fmt.Sprintf("v0.%v", (time.Now().UnixNano())), false) + expVersion := fmt.Sprintf("v0.%v", (time.Now().UnixNano())) + expCreatedOn := uint64(time.Now().Unix()) + newpolicy.Rotate(t, dut, expCreatedOn, expVersion, false) // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get t.Run("Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get", func(t *testing.T) { - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) }) // Fetch the Desired Authorization Policy and Attach base Admin Policy Before Rotate @@ -518,20 +737,33 @@ func TestAuthz2(t *testing.T) { t.Logf("Authz.Rotate upload was successful, receiving response ...") } _, err = rotateStream.Recv() - if err != nil { - t.Fatalf("Expected Error while receiving rotate request reply %v", err) + if err == nil { + t.Fatalf("Expected Error while receiving rotate request reply for invalid policy, got nil") } - t.Run("Verification of Policy for read_only user to deny gRIBI Get before closing stream", func(t *testing.T) { - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + t.Run("Verification of Policy for read_only user to allow gRIBI Get and deny gNMI Get before closing stream", func(t *testing.T) { + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) }) // Close the Stream rotateStream.CloseSend() + + // Verify telemetry rolled back + serverName := "DEFAULT" + versionTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyVersion().State()) + createdOnTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyCreatedOn().State()) + if versionTele != expVersion { + t.Errorf("Expected rolled back telemetry version %s, got %s", expVersion, versionTele) + } + if createdOnTele != expCreatedOn { + t.Errorf("Expected rolled back telemetry createdOn %d, got %d", expCreatedOn, createdOnTele) + } + // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get t.Run("Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get after closing stream", func(t *testing.T) { - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) }) }) @@ -584,16 +816,16 @@ func TestAuthz2(t *testing.T) { } // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get t.Run("Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get after rotate without force overwrite", func(t *testing.T) { - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) }) t.Logf("Preforming Rotate with the same version with force overwrite\n") newpolicy.Rotate(t, dut, uint64(time.Now().Unix()), prevVersion, true) // Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get t.Run("Verification of Policy for read_only to allow gRIBI Get and to deny gNMI Get after rotate wth force overwrite", func(t *testing.T) { - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) - authz.Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GribiGet, &authz.ExceptDeny{}, &authz.HardVerify{}) + Verify(t, dut, spiffeCertReadOnly, gnxi.RPCs.GnmiGet, &authz.HardVerify{}) }) }) @@ -628,17 +860,45 @@ func TestAuthz3(t *testing.T) { // Version and Created On Field Verification t.Logf("Performing Authz.Get request on device %s", dut.Name()) gnsiC := dut.RawAPIs().GNSI(t) + + serverName := "DEFAULT" + getCounterPath := gnmi.OC().System().GrpcServer(serverName).AuthzPolicyCounters().Rpc("/gnsi.authz.v1.Authz/Get").AccessAccepts().State() + getVal := func() uint64 { + val, ok := gnmi.Lookup(t, dut, getCounterPath).Val() + if !ok { + return 0 + } + return val + } + valBefore := getVal() + resp, err := gnsiC.Authz().Get(context.Background(), &authzpb.GetRequest{}) if err != nil { t.Fatalf("Authz.Get request is failed on device %s", dut.Name()) } t.Logf("Authz.Get response is %s", resp) + + valAfter := getVal() + if valAfter != valBefore+1 { + t.Errorf("Expected gNSI.Get counter to increment from %d to %d, got %d", valBefore, valBefore+1, valAfter) + } + if resp.GetVersion() != expVersion { t.Errorf("Version has Changed in Authz.Get response") } if resp.GetCreatedOn() != expCreatedOn { t.Errorf("CreatedOn Value has Changed in Authz.Get response") } + + // Telemetry validation + versionTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyVersion().State()) + createdOnTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyCreatedOn().State()) + if versionTele != expVersion { + t.Errorf("Expected telemetry version %s after 30s, got %s", expVersion, versionTele) + } + if createdOnTele != expCreatedOn { + t.Errorf("Expected telemetry createdOn %d after 30s, got %d", expCreatedOn, createdOnTele) + } if !cmp.Equal(&newpolicy, finalPolicy) { t.Fatalf("Not Expecting Policy Mismatch before and after the Wait):\n%s", cmp.Diff(&newpolicy, finalPolicy)) } @@ -717,17 +977,54 @@ func TestAuthz4(t *testing.T) { if err != nil { t.Fatalf("Could not create GNSI Connection %v", err) } + + serverName := "DEFAULT" + getCounterPath := gnmi.OC().System().GrpcServer(serverName).AuthzPolicyCounters().Rpc("/gnsi.authz.v1.Authz/Get").AccessAccepts().State() + getVal := func() uint64 { + val, ok := gnmi.Lookup(t, dut, getCounterPath).Val() + if !ok { + return 0 + } + return val + } + valBefore := getVal() + resp, err := gnsiC.Authz().Get(context.Background(), &authzpb.GetRequest{}) if err != nil { t.Fatalf("Authz.Get request is failed with Error %v", err) } t.Logf("Authz.Get response is %s", resp) + + valAfter := getVal() + if valAfter != valBefore+1 { + t.Errorf("Expected gNSI.Get counter to increment from %d to %d, got %d", valBefore, valBefore+1, valAfter) + } + if resp.GetVersion() != expVersion { t.Errorf("Version has Changed to %v from Expected Version %v after Reboot Trigger", resp.GetVersion(), expVersion) } if resp.GetCreatedOn() != expCreatedOn { t.Errorf("Created On has Changed to %v from Expected Created On %v after Reboot Trigger", resp.GetCreatedOn(), expCreatedOn) } + + var finalPolicy authz.AuthorizationPolicy + err = json.Unmarshal([]byte(resp.GetPolicy()), &finalPolicy) + if err != nil { + t.Fatalf("Failed to unmarshal policy content after reboot: %v", err) + } + if !cmp.Equal(&newpolicy, &finalPolicy) { + t.Fatalf("Policy content mismatch after reboot. Diff:\n%s", cmp.Diff(&newpolicy, &finalPolicy)) + } + + // Telemetry validation after reboot + versionTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyVersion().State()) + createdOnTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyCreatedOn().State()) + if versionTele != expVersion { + t.Errorf("Expected telemetry version %s after reboot, got %s", expVersion, versionTele) + } + if createdOnTele != expCreatedOn { + t.Errorf("Expected telemetry createdOn %d after reboot, got %d", expCreatedOn, createdOnTele) + } // Verify all results match per the above table for policy policy-normal-1 setUpBaseline(t, dut) verifyAuthTable(t, dut, authTable) diff --git a/feature/gnsi/authz/tests/authz/metadata.textproto b/feature/gnsi/authz/tests/authz/metadata.textproto index 757ee4864e8..515a8d0c3e6 100644 --- a/feature/gnsi/authz/tests/authz/metadata.textproto +++ b/feature/gnsi/authz/tests/authz/metadata.textproto @@ -3,5 +3,5 @@ uuid: "a5c2d46c-976b-41bd-9fae-3341ce5c1b29" plan_id: "Authz" -description: "General Authz (1-4) tests" +description: "General Authz tests" testbed: TESTBED_DUT diff --git a/feature/gnsi/authz/tests/authz/testdata/policy.json b/feature/gnsi/authz/tests/authz/testdata/policy.json index b3491e11aff..ddfcaf89606 100644 --- a/feature/gnsi/authz/tests/authz/testdata/policy.json +++ b/feature/gnsi/authz/tests/authz/testdata/policy.json @@ -225,4 +225,31 @@ } } ] + }, + { + "name": "policy-prefix-suffix-match", + "allow_rules": [ + { + "name": "prefix-match-xyz", + "source": { + "principals": [ + "spiffe://test-abc.foo.bar/xyz/*" + ] + }, + "request": { + "paths": ["/gnmi.gNMI/Get"] + } + }, + { + "name": "suffix-match-admin", + "source": { + "principals": [ + "*admin" + ] + }, + "request": { + "paths": ["/gribi.gRIBI/Get"] + } + } + ] }] diff --git a/internal/security/authz/authz.go b/internal/security/authz/authz.go index a2bd10919ab..d78686668b6 100644 --- a/internal/security/authz/authz.go +++ b/internal/security/authz/authz.go @@ -35,6 +35,7 @@ import ( "google.golang.org/grpc/status" authzpb "github.com/openconfig/gnsi/authz" + "github.com/openconfig/ondatra/gnmi" ) // Spiffe is an struct to save an Spiffe id and its svid. @@ -100,8 +101,14 @@ func (p *AuthorizationPolicy) Marshal() ([]byte, error) { return json.Marshal(p) } +// Rotate apply policy p on device dut, this is test api for positive testing and it fails the test on failure. // Rotate apply policy p on device dut, this is test api for positive testing and it fails the test on failure. func (p *AuthorizationPolicy) Rotate(t *testing.T, dut *ondatra.DUTDevice, createdOn uint64, version string, forcOverwrite bool) { + p.RotateWithOptions(t, dut, createdOn, version, forcOverwrite, nil) +} + +// RotateWithOptions applies policy p on device dut with optional callback before finalization and performs telemetry validation. +func (p *AuthorizationPolicy) RotateWithOptions(t *testing.T, dut *ondatra.DUTDevice, createdOn uint64, version string, forcOverwrite bool, beforeFinalize func()) { t.Logf("Performing Authz.Rotate request on device %s", dut.Name()) gnsiC, err := dut.RawAPIs().BindingDUT().DialGNSI(context.Background()) if err != nil { @@ -138,6 +145,23 @@ func (p *AuthorizationPolicy) Rotate(t *testing.T, dut *ondatra.DUTDevice, creat if !cmp.Equal(p, tempPolicy) { t.Fatalf("Policy after upload (temporary) is not the same as the one upload, diff is: %v", cmp.Diff(p, tempPolicy)) } + + // Validate telemetry metadata (temporary) + serverName := "DEFAULT" + versionTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyVersion().State()) + createdOnTele := gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyCreatedOn().State()) + if versionTele != version { + t.Errorf("Expected telemetry version %s, got %s", version, versionTele) + } + if createdOnTele != createdOn { + t.Errorf("Expected telemetry createdOn %d, got %d", createdOn, createdOnTele) + } + + // Call the callback if provided + if beforeFinalize != nil { + beforeFinalize() + } + finalizeRotateReq := &authzpb.RotateAuthzRequest_FinalizeRotation{FinalizeRotation: &authzpb.FinalizeRequest{}} err = rotateStream.Send(&authzpb.RotateAuthzRequest{RotateRequest: finalizeRotateReq}) t.Logf("Sending Authz.Rotate FinalizeRotation request: \n%s", prettyPrint(finalizeRotateReq)) @@ -149,6 +173,15 @@ func (p *AuthorizationPolicy) Rotate(t *testing.T, dut *ondatra.DUTDevice, creat t.Fatalf("Policy after upload (temporary) is not the same as the one upload, diff is: %v", cmp.Diff(p, finalPolicy)) } + // Validate telemetry metadata (finalized) + versionTele = gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyVersion().State()) + createdOnTele = gnmi.Get(t, dut, gnmi.OC().System().GrpcServer(serverName).AuthenticationPolicyCreatedOn().State()) + if versionTele != version { + t.Errorf("Expected telemetry version %s, got %s after finalize", version, versionTele) + } + if createdOnTele != createdOn { + t.Errorf("Expected telemetry createdOn %d, got %d after finalize", createdOn, createdOnTele) + } } // NewAuthorizationPolicy creates an empty policy. @@ -214,8 +247,8 @@ func (p *AuthorizationPolicy) PrettyPrint(t *testing.T) string { return string(prettyTex) } -type verifyOpt interface { - isVerifyOpt() +type VerifyOpt interface { + IsVerifyOpt() } // ExceptDeny is passed to verify function when failure is expected. @@ -227,12 +260,12 @@ type ExceptDeny struct { type HardVerify struct { } -func (o *ExceptDeny) isVerifyOpt() {} -func (o *HardVerify) isVerifyOpt() {} +func (o *ExceptDeny) IsVerifyOpt() {} +func (o *HardVerify) IsVerifyOpt() {} // Verify uses prob to validate if the user access for a certain rpc is expected. // It also execute the rpc when HardVerif is passed and verifies if it matches the expectation. -func Verify(t testing.TB, dut *ondatra.DUTDevice, spiffe *Spiffe, rpc *gnxi.RPC, opts ...verifyOpt) { +func Verify(t testing.TB, dut *ondatra.DUTDevice, spiffe *Spiffe, rpc *gnxi.RPC, opts ...VerifyOpt) { expectedRes := authzpb.ProbeResponse_ACTION_PERMIT expectedExecErr := codes.OK hardVerify := false @@ -242,7 +275,7 @@ func Verify(t testing.TB, dut *ondatra.DUTDevice, spiffe *Spiffe, rpc *gnxi.RPC, expectedRes = authzpb.ProbeResponse_ACTION_DENY expectedExecErr = codes.PermissionDenied case *HardVerify: - hardVerify = false + hardVerify = true default: t.Errorf("Invalid option is passed to Verify function: %T", opt) }