From 325919bdc196487595366a7eb1316d6e3df3a4ff Mon Sep 17 00:00:00 2001 From: "Dipesh Chauhan (Nokia)" Date: Wed, 21 Aug 2024 12:25:42 -0400 Subject: [PATCH 1/4] Accountz-3.1 "This code is a Contribution to the OpenConfig Feature Profiles project ("Work") made under the Google Software Grant and Corporate Contributor License Agreement ("CLA") and governed by the Apache License 2.0. No other rights or licenses in or to any of Nokia's intellectual property are granted for any other purpose. This code is provided on an "as is" basis without any warranties of any kind." --- feature/security/gnsi/acctz/README.md | 2 +- .../record_subscribe_non_grpc}/README.md | 2 +- .../metadata.textproto | 15 + .../record_subscribe_non_grpc_test.go | 702 ++++++++++++++++++ 4 files changed, 719 insertions(+), 2 deletions(-) rename feature/security/gnsi/acctz/{RecordSubscribeNongrpc => tests/record_subscribe_non_grpc}/README.md (98%) create mode 100644 feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/metadata.textproto create mode 100644 feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go diff --git a/feature/security/gnsi/acctz/README.md b/feature/security/gnsi/acctz/README.md index 3232624cf89..b814c93a42f 100644 --- a/feature/security/gnsi/acctz/README.md +++ b/feature/security/gnsi/acctz/README.md @@ -35,7 +35,7 @@ Create a library of device configuration to be used for all of the gNSI.acctz.v1 ## gNSI Accounting (acctz) Tests: - [ACCTZ-1.1 Record Subscribe Full](RecordSubscribeFull) - [ACCTZ-2.1 Record Subscribe Partial](RecordSubscribePartial) -- [ACCTZ-3.1 Record Subscribe Non-gRPC](RecordSubscribeNongrpc) +- [ACCTZ-3.1 Record Subscribe Non-gRPC](tests/record_subscribe_non_grpc) - [ACCTZ-4.1 Record History Truncation](RecordHistoryTruncation/) - [ACCTZ-4.2 Record Payload Truncation](RecordPayloadTruncation/) - [ACCTZ-5.1 Record Subscribe Idle Timeout](RecordSubscribeIdleTimeout/) diff --git a/feature/security/gnsi/acctz/RecordSubscribeNongrpc/README.md b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/README.md similarity index 98% rename from feature/security/gnsi/acctz/RecordSubscribeNongrpc/README.md rename to feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/README.md index 74a7e7aaad7..6ca2e7f8fc1 100644 --- a/feature/security/gnsi/acctz/RecordSubscribeNongrpc/README.md +++ b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/README.md @@ -1,4 +1,4 @@ -# ACCTZ-3.1 - gNSI.acctz.v1 (Accounting) Test Record Subscribe Non-gRPC +# ACCTZ-3.1: Record Subscribe Non-gRPC ## Summary Test Accounting for non-gRPC records diff --git a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/metadata.textproto b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/metadata.textproto new file mode 100644 index 00000000000..0b39086d281 --- /dev/null +++ b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/metadata.textproto @@ -0,0 +1,15 @@ +# proto-file: github.com/openconfig/featureprofiles/proto/metadata.proto +# proto-message: Metadata + +uuid: "036d3d49-00dd-46ff-abe6-afc20768db6f" +plan_id: "ACCTZ-3.1" +description: "Record Subscribe Non-gRPC" +testbed: TESTBED_DUT +platform_exceptions: { + platform: { + vendor: NOKIA + } + deviations: { + set_native_user: true + } +} diff --git a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go new file mode 100644 index 00000000000..2589cb65816 --- /dev/null +++ b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go @@ -0,0 +1,702 @@ +package record_subscribe_non_grpc_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "strconv" + "strings" + "testing" + "time" + + "github.com/openconfig/featureprofiles/internal/deviations" + "github.com/openconfig/featureprofiles/internal/fptest" + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/gnsi/acctz" + tpb "github.com/openconfig/kne/proto/topo" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/binding" + ondatragnmi "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "golang.org/x/crypto/ssh" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + successUsername = "acctztestuser" + successPassword = "verysecurepassword" + successRoleName = "acctz-fp-test-success" + failUsername = "bilbo" + failPassword = "baggins" + failRoleName = "acctz-fp-test-fail" + command = "show version" + failCommand = "show version" + shellCommand = "uname -a" +) + +type rpcRecord struct { + startTime time.Time + doneTime time.Time + cmdType acctz.CommandService_CmdServiceType + rpcPath string + localIp string + localPort uint32 + remoteIp string + remotePort uint32 + succeeded bool + expectedStatus acctz.SessionInfo_SessionStatus + expectedAuthenType acctz.AuthnDetail_AuthnType + expectedAuthenStatus acctz.AuthnDetail_AuthnStatus + expectedAuthenCause string + expectedIdentity string + expectedRole string +} + +type recordRequestResult struct { + record *acctz.RecordResponse + err error +} + +func TestMain(m *testing.M) { + fptest.RunTests(m) +} + +func createNativeRole(t testing.TB, dut *ondatra.DUTDevice) { + var SetRequest *gpb.SetRequest + switch dut.Vendor() { + case ondatra.NOKIA: + successRoleData, err := json.Marshal([]any{ + map[string]any{ + "services": []string{"cli"}, + }, + }) + if err != nil { + t.Fatalf("Error with json Marshal: %v", err) + } + + failRoleData, err := json.Marshal([]any{ + map[string]any{ + "services": []string{"cli"}, + "cli": map[string][]string{ + "deny-command-list": {"show version"}, + }, + }, + }) + if err != nil { + t.Fatalf("Error with json Marshal: %v", err) + } + + successUserData, err := json.Marshal([]any{ + map[string]any{ + "password": successPassword, + "role": []string{successRoleName}, + }, + }) + if err != nil { + t.Fatalf("Error with json Marshal: %v", err) + } + + failUserData, err := json.Marshal([]any{ + map[string]any{ + "password": failPassword, + "role": []string{failRoleName}, + }, + }) + if err != nil { + t.Fatalf("Error with json Marshal: %v", err) + } + + SetRequest = &gpb.SetRequest{ + Prefix: &gpb.Path{ + Origin: "native", + }, + Replace: []*gpb.Update{ + { + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "system"}, + {Name: "aaa"}, + {Name: "authorization"}, + {Name: "role", Key: map[string]string{"rolename": successRoleName}}, + }, + }, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: successRoleData, + }, + }, + }, + { + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "system"}, + {Name: "aaa"}, + {Name: "authorization"}, + {Name: "role", Key: map[string]string{"rolename": failRoleName}}, + }, + }, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: failRoleData, + }, + }, + }, + { + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "system"}, + {Name: "aaa"}, + {Name: "authentication"}, + {Name: "user", Key: map[string]string{"username": successUsername}}, + }, + }, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: successUserData, + }, + }, + }, + { + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "system"}, + {Name: "aaa"}, + {Name: "authentication"}, + {Name: "user", Key: map[string]string{"username": failUsername}}, + }, + }, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: failUserData, + }, + }, + }, + }, + } + default: + t.Fatalf("Unsupported vendor %s for deviation 'deviation_native_users'", dut.Vendor()) + } + gnmiClient := dut.RawAPIs().GNMI(t) + if _, err := gnmiClient.Set(context.Background(), SetRequest); err != nil { + t.Fatalf("Unexpected error configuring User: %v", err) + } +} + +func setupUsers(t *testing.T, dut *ondatra.DUTDevice) { + auth := &oc.System_Aaa_Authentication{} + auth.GetOrCreateUser(successUsername) + auth.GetOrCreateUser(failUsername) + + ondatragnmi.Update(t, dut, ondatragnmi.OC().System().Aaa().Authentication().Config(), auth) + + if deviations.SetNativeUser(dut) { + // probably all vendors need to handle this since the user should have a role attached to + // it allowing us to login via ssh/console/whatever + createNativeRole(t, dut) + } +} + +func dialSSH(t *testing.T, username, password, addr string, port uint32) (net.Conn, io.Writer, io.Reader) { + tcpConn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", addr, port), 0) + if err != nil { + t.Fatalf("got unexpected error dialing ssh tcp connection, error: %s", err) + } + + cConn, chans, reqs, err := ssh.NewClientConn( + tcpConn, + fmt.Sprintf("%s:%d", addr, port), + &ssh.ClientConfig{ + User: username, + Auth: []ssh.AuthMethod{ + ssh.Password(password), + ssh.KeyboardInteractive( + func(user, instruction string, questions []string, echos []bool) ([]string, error) { + answers := make([]string, len(questions)) + for i := range answers { + answers[i] = password + } + + return answers, nil + }, + ), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + }, + ) + if err != nil { + t.Fatalf("got unexpected error dialing ssh, error: %s", err) + } + + // stdin/stdout so we get a tty allocated + conn := ssh.NewClient(cConn, chans, reqs) + + sess, err := conn.NewSession() + if err != nil { + t.Fatalf("failed creating ssh session, error: %s", err) + } + + w, err := sess.StdinPipe() + if err != nil { + t.Fatal(err) + } + + r, err := sess.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + term := ssh.TerminalModes{ + ssh.ECHO: 1, + ssh.TTY_OP_ISPEED: 115200, + ssh.TTY_OP_OSPEED: 115200, + } + + err = sess.RequestPty( + "xterm", + 255, + 80, + term, + ) + if err != nil { + t.Fatal(err) + } + + err = sess.Shell() + if err != nil { + t.Fatal(err) + } + + return tcpConn, w, r +} + +func sendCLICommand(t *testing.T, addr string, port uint32) []rpcRecord { + var records []rpcRecord + + tcpConn, w, _ := dialSSH(t, successUsername, successPassword, addr, port) + defer func() { + // give things a second to percolate then close the connection + time.Sleep(3 * time.Second) + + err := tcpConn.Close() + if err != nil { + t.Logf("error closing tcp(ssh) connection, will ignore, error: %s", err) + } + }() + + startTime := time.Now() + + time.Sleep(time.Second) + + // this might not work for other vendors, so probably we can have a switch here and pass + // the writer to func per vendor if needed + _, err := w.Write([]byte(fmt.Sprintf("%s\n", command))) + if err != nil { + t.Fatalf("failed sending cli command, error: %s", err) + } + + addrParts := strings.Split(tcpConn.LocalAddr().String(), ":") + remoteAddr := addrParts[0] + remotePort, _ := strconv.Atoi(addrParts[1]) + + resolvedAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Fatalf("failed resolving ssh destination addr, error: %s", err) + } + + addr = resolvedAddr.IP.String() + + records = append(records, rpcRecord{ + startTime: startTime, + doneTime: time.Now(), + cmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, + localIp: addr, + localPort: port, + remoteIp: remoteAddr, + remotePort: uint32(remotePort), + succeeded: true, + expectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, + expectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + expectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, + expectedAuthenCause: "authentication_method: local", + expectedIdentity: successUsername, + expectedRole: successRoleName, + }) + + return records +} + +func sendCLICommandFail(t *testing.T, addr string, port uint32) []rpcRecord { + var records []rpcRecord + + tcpConn, w, _ := dialSSH(t, failUsername, failPassword, addr, port) + defer func() { + // give things a second to percolate then close the connection + time.Sleep(3 * time.Second) + + err := tcpConn.Close() + if err != nil { + t.Logf("error closing tcp(ssh) connection, will ignore, error: %s", err) + } + }() + + startTime := time.Now() + + time.Sleep(time.Second) + + _, err := w.Write([]byte(fmt.Sprintf("%s\n", failCommand))) + if err != nil { + t.Fatalf("failed sending cli command, error: %s", err) + } + + addrParts := strings.Split(tcpConn.LocalAddr().String(), ":") + remoteAddr := addrParts[0] + remotePort, _ := strconv.Atoi(addrParts[1]) + + resolvedAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Fatalf("failed resolving ssh destination addr, error: %s", err) + } + + addr = resolvedAddr.IP.String() + + records = append(records, rpcRecord{ + startTime: startTime, + doneTime: time.Now(), + cmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, + localIp: addr, + localPort: port, + remoteIp: remoteAddr, + remotePort: uint32(remotePort), + succeeded: true, + expectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, + expectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + expectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, + expectedAuthenCause: "authentication_method: local", + expectedIdentity: failUsername, + expectedRole: failRoleName, + }) + + return records +} + +func sendShellCommand(t *testing.T, dut *ondatra.DUTDevice, addr string, port uint32) []rpcRecord { + var records []rpcRecord + + shellUsername := successUsername + shellPassword := successPassword + + switch dut.Vendor() { + case ondatra.NOKIA: + // assuming linuxadmin is present and ssh'ing directly via this user gets us to shell + // straight away so this is easy button to trigger a shell record + shellUsername = "linuxadmin" + shellPassword = "NokiaSrl1!" + } + + tcpConn, w, _ := dialSSH(t, shellUsername, shellPassword, addr, port) + defer func() { + // give things a second to percolate then close the connection + time.Sleep(3 * time.Second) + + err := tcpConn.Close() + if err != nil { + t.Logf("error closing tcp(ssh) connection, will ignore, error: %s", err) + } + }() + + startTime := time.Now() + + // this might not work for other vendors, so probably we can have a switch here and pass + // the writer to func per vendor if needed + _, err := w.Write([]byte(fmt.Sprintf("%s\n", shellCommand))) + if err != nil { + t.Fatalf("failed sending cli command, error: %s", err) + } + + addrParts := strings.Split(tcpConn.LocalAddr().String(), ":") + remoteAddr := addrParts[0] + remotePort, _ := strconv.Atoi(addrParts[1]) + + resolvedAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Fatalf("failed resolving ssh destination addr, error: %s", err) + } + + addr = resolvedAddr.IP.String() + + records = append(records, rpcRecord{ + startTime: startTime, + doneTime: time.Now(), + cmdType: acctz.CommandService_CMD_SERVICE_TYPE_SHELL, + localIp: addr, + localPort: port, + remoteIp: remoteAddr, + remotePort: uint32(remotePort), + succeeded: true, + expectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, + expectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + expectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + expectedAuthenCause: "", + expectedIdentity: shellUsername, + }) + + return records +} + +func getDutAddr(t *testing.T, dut *ondatra.DUTDevice) string { + var serviceDUT interface { + Service(string) (*tpb.Service, error) + } + + err := binding.DUTAs(dut.RawAPIs().BindingDUT(), &serviceDUT) + if err != nil { + t.Log("DUT does not support `Service` function, will attempt to use dut name field") + + return dut.Name() + } + + dutSSHService, err := serviceDUT.Service("ssh") + if err != nil { + t.Fatal(err) + } + + return dutSSHService.GetOutsideIp() +} + +func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { + dut := ondatra.DUT(t, "dut") + + setupUsers(t, dut) + + // https://github.com/openconfig/featureprofiles/issues/2637 + // basically, just waiting to see what the "best"/"preferred" way is to get the v4/v6 of the + // dut -- for now we use this hacky work around because ssh isn't exposed in introspection anyway + // so... we get what we can get. + addr := getDutAddr(t, dut) + + var records []rpcRecord + + // put enough time between the test starting and any prior events so we can easily know where + // our records start + time.Sleep(5 * time.Second) + + startTime := time.Now() + + // suppose ssh could be not 22 in some cases but don't think this is exposed by introspect + newRecords := sendCLICommand(t, addr, 22) + records = append(records, newRecords...) + + newRecords = sendCLICommandFail(t, addr, 22) + records = append(records, newRecords...) + + newRecords = sendShellCommand(t, dut, addr, 22) + records = append(records, newRecords...) + + // quick sleep to ensure all the records have been processed/ready for us + time.Sleep(5 * time.Second) + + // get gnsi record subscribe client + acctzClient := dut.RawAPIs().GNSI(t).Acctz() + + acctzSubClient, err := acctzClient.RecordSubscribe(context.Background()) + if err != nil { + t.Fatalf("failed getting accountz record subscribe client, error: %s", err) + } + + // this will have to move up to RecordSubscribe call after this is brought into fp/ondatra stuff + // https://github.com/openconfig/gnsi/pull/149/files + err = acctzSubClient.Send(&acctz.RecordRequest{ + Timestamp: ×tamppb.Timestamp{ + Seconds: 0, + Nanos: 0, + }, + }) + if err != nil { + t.Fatalf("failed sending accountz record request, error: %s", err) + } + + var recordIdx int + + var lastTimestampUnixMillis int64 + var lastTaskID string + + for { + if recordIdx >= len(records) { + t.Log("out of records to process...") + + break + } + + r := make(chan recordRequestResult) + + go func(r chan recordRequestResult) { + var response *acctz.RecordResponse + + response, err = acctzSubClient.Recv() + + r <- recordRequestResult{ + record: response, + err: err, + } + }(r) + + var done bool + + var resp recordRequestResult + + select { + case rr := <-r: + resp = rr + case <-time.After(10 * time.Second): + done = true + } + + if done { + t.Log("done receiving records...") + + break + } + + if resp.err != nil { + t.Fatalf("failed receiving record response, error: %s", resp.err) + } + + if resp.record.GetHistoryIstruncated() { + t.Fatal("history is truncated but it shouldnt be") + } + + if !resp.record.Timestamp.AsTime().After(startTime) { + // skipping record, was before test start time + continue + } + + // check that the timestamp for the record is between our start/stop times for our rpc + timestamp := resp.record.Timestamp.AsTime() + + if timestamp.UnixMilli() == lastTimestampUnixMillis { + // this ensures that timestamps are actually changing for each record + t.Fatalf("timestamp is the same as the previous timestamp, this shouldnt be possible!") + } + + lastTimestampUnixMillis = timestamp.UnixMilli() + + // some task ids may be tracked multiple times (for start/stop accounting). if we see two in + // a row that are the same task we know this is what's up and we can skip this record and + // continue + currentTaskID := resp.record.TaskIds[0] + if currentTaskID == lastTaskID { + continue + } + + lastTaskID = currentTaskID + + if records[recordIdx].startTime.Unix() > timestamp.Unix() { + t.Fatalf( + "record timestamp is prior to rpc start time timestamp, rpc start timestamp %d, record timestamp %d", + records[recordIdx].startTime.Unix(), + timestamp.Unix(), + ) + } + + // done time (that we recorded when making the rpc) + 2 second for some breathing room + if records[recordIdx].doneTime.Unix()+2 < timestamp.Unix() { + t.Fatalf( + "record timestamp is after rpc end timestamp, rpc end timestamp %d, record timestamp %d", + records[recordIdx].doneTime.Unix()+2, + timestamp.Unix(), + ) + } + + cmdType := resp.record.GetCmdService().GetServiceType() + + if records[recordIdx].cmdType != cmdType { + t.Fatalf("service type not correct, got %q, want %q", cmdType, records[recordIdx].cmdType) + } + + servicePath := resp.record.GetGrpcService().GetRpcName() + if records[recordIdx].rpcPath != servicePath { + t.Fatalf("service path not correct, got %q, want %q", servicePath, records[recordIdx].rpcPath) + } + + channelID := resp.record.GetSessionInfo().GetChannelId() + + // this channel check maybe should just go away entirely -- see: + // https://github.com/openconfig/gnsi/issues/98 + // in case of nokia this is being set to the aaa session id just to have some hopefully + // useful info in this field to identify a "session" (even if it isn't necessarily ssh/grpc + // directly) + if !records[recordIdx].succeeded { + if channelID != "aaa_session_id: 0" { + t.Fatalf("auth was not successful for this record, but channel id was set, got %q", channelID) + } + } + + // status + sessionStatus := resp.record.GetSessionInfo().GetStatus() + if records[recordIdx].expectedStatus != sessionStatus { + t.Fatalf("session status not correct, got %q, want %q", sessionStatus, records[recordIdx].expectedStatus) + } + + // authen type + authenType := resp.record.GetSessionInfo().GetAuthn().GetType() + if records[recordIdx].expectedAuthenType != authenType { + t.Fatalf("authenType not correct, got %q, want %q", authenType, records[recordIdx].expectedAuthenType) + } + + authenStatus := resp.record.GetSessionInfo().GetAuthn().GetStatus() + if records[recordIdx].expectedAuthenStatus != authenStatus { + t.Fatalf("authenStatus not correct, got %q, want %q", authenStatus, records[recordIdx].expectedAuthenStatus) + } + + authenCause := resp.record.GetSessionInfo().GetAuthn().GetCause() + if records[recordIdx].expectedAuthenCause != authenCause { + t.Fatalf("authenCause not correct, got %q, want %q", authenCause, records[recordIdx].expectedAuthenCause) + } + + userIdentity := resp.record.GetSessionInfo().GetUser().GetIdentity() + if records[recordIdx].expectedIdentity != userIdentity { + t.Fatalf("identity not correct, got %q, want %q", userIdentity, records[recordIdx].expectedIdentity) + } + + if !records[recordIdx].succeeded { + // not a successful rpc so don't need to check anything else + recordIdx++ + + continue + } + + role := resp.record.GetSessionInfo().GetUser().GetRole() + if records[recordIdx].expectedRole != role { + t.Fatalf("role not correct, got %q, want %q", role, records[recordIdx].expectedRole) + } + + // verify the l4 bits align, this stuff is only set if auth is successful so do it down here + localAddr := resp.record.GetSessionInfo().GetLocalAddress() + if records[recordIdx].localIp != localAddr { + t.Fatalf("local address not correct, got %q, want %q", localAddr, records[recordIdx].localIp) + } + + localPort := resp.record.GetSessionInfo().GetLocalPort() + if records[recordIdx].localPort != localPort { + t.Fatalf("local port not correct, got %d, want %d", localPort, records[recordIdx].localPort) + } + + remoteAddr := resp.record.GetSessionInfo().GetRemoteAddress() + if records[recordIdx].remoteIp != remoteAddr { + t.Fatalf("remote address not correct, got %q, want %q", remoteAddr, records[recordIdx].remoteIp) + } + + remotePort := resp.record.GetSessionInfo().GetRemotePort() + if records[recordIdx].remotePort != remotePort { + t.Fatalf("remote port not correct, got %d, want %d", remotePort, records[recordIdx].remotePort) + } + + recordIdx++ + } + + if recordIdx != len(records) { + t.Fatal("did not process all records") + } +} From e7ebba05b1a0d0419b792e9655849239c83f9c19 Mon Sep 17 00:00:00 2001 From: "Dipesh Chauhan (Nokia)" Date: Wed, 28 Aug 2024 15:55:53 -0400 Subject: [PATCH 2/4] Refactor code "This code is a Contribution to the OpenConfig Feature Profiles project ("Work") made under the Google Software Grant and Corporate Contributor License Agreement ("CLA") and governed by the Apache License 2.0. No other rights or licenses in or to any of Nokia's intellectual property are granted for any other purpose. This code is provided on an "as is" basis without any warranties of any kind." --- .../metadata.textproto | 10 +- .../record_subscribe_non_grpc_test.go | 365 ++++++++---------- 2 files changed, 171 insertions(+), 204 deletions(-) diff --git a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/metadata.textproto b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/metadata.textproto index 0b39086d281..a9a183ff323 100644 --- a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/metadata.textproto +++ b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/metadata.textproto @@ -4,12 +4,4 @@ uuid: "036d3d49-00dd-46ff-abe6-afc20768db6f" plan_id: "ACCTZ-3.1" description: "Record Subscribe Non-gRPC" -testbed: TESTBED_DUT -platform_exceptions: { - platform: { - vendor: NOKIA - } - deviations: { - set_native_user: true - } -} +testbed: TESTBED_DUT \ No newline at end of file diff --git a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go index 2589cb65816..852256ad212 100644 --- a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go +++ b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go @@ -6,12 +6,11 @@ import ( "fmt" "io" "net" - "strconv" - "strings" "testing" "time" - "github.com/openconfig/featureprofiles/internal/deviations" + "github.com/openconfig/gnsi/credentialz" + "github.com/openconfig/featureprofiles/internal/fptest" gpb "github.com/openconfig/gnmi/proto/gnmi" "github.com/openconfig/gnsi/acctz" @@ -27,13 +26,13 @@ import ( const ( successUsername = "acctztestuser" successPassword = "verysecurepassword" - successRoleName = "acctz-fp-test-success" failUsername = "bilbo" failPassword = "baggins" failRoleName = "acctz-fp-test-fail" command = "show version" failCommand = "show version" shellCommand = "uname -a" + sshPort = 22 ) type rpcRecord struct { @@ -51,7 +50,6 @@ type rpcRecord struct { expectedAuthenStatus acctz.AuthnDetail_AuthnStatus expectedAuthenCause string expectedIdentity string - expectedRole string } type recordRequestResult struct { @@ -63,150 +61,127 @@ func TestMain(m *testing.M) { fptest.RunTests(m) } -func createNativeRole(t testing.TB, dut *ondatra.DUTDevice) { - var SetRequest *gpb.SetRequest - switch dut.Vendor() { - case ondatra.NOKIA: - successRoleData, err := json.Marshal([]any{ - map[string]any{ - "services": []string{"cli"}, - }, - }) - if err != nil { - t.Fatalf("Error with json Marshal: %v", err) - } - - failRoleData, err := json.Marshal([]any{ - map[string]any{ - "services": []string{"cli"}, - "cli": map[string][]string{ - "deny-command-list": {"show version"}, +func setupUserPassword(t *testing.T, dut *ondatra.DUTDevice, username, password string) { + request := &credentialz.RotateAccountCredentialsRequest{ + Request: &credentialz.RotateAccountCredentialsRequest_Password{ + Password: &credentialz.PasswordRequest{ + Accounts: []*credentialz.PasswordRequest_Account{ + { + Account: username, + Password: &credentialz.PasswordRequest_Password{ + Value: &credentialz.PasswordRequest_Password_Plaintext{ + Plaintext: password, + }, + }, + Version: "v1.0", + CreatedOn: uint64(time.Now().Unix()), + }, }, }, - }) - if err != nil { - t.Fatalf("Error with json Marshal: %v", err) - } + }, + } - successUserData, err := json.Marshal([]any{ - map[string]any{ - "password": successPassword, - "role": []string{successRoleName}, - }, - }) - if err != nil { - t.Fatalf("Error with json Marshal: %v", err) - } + credzClient := dut.RawAPIs().GNSI(t).Credentialz() - failUserData, err := json.Marshal([]any{ - map[string]any{ - "password": failPassword, - "role": []string{failRoleName}, - }, - }) - if err != nil { - t.Fatalf("Error with json Marshal: %v", err) - } + credzRotateClient, err := credzClient.RotateAccountCredentials(context.Background()) + if err != nil { + t.Fatalf("failed fetching credentialz rotate account credentials client, error: %s", err) + } - SetRequest = &gpb.SetRequest{ - Prefix: &gpb.Path{ - Origin: "native", + err = credzRotateClient.Send(request) + if err != nil { + t.Fatalf("failed sending credentialz rotate account credentials request, error: %s", err) + } + + _, err = credzRotateClient.Recv() + if err != nil { + t.Fatalf("failed receiving credentialz rotate account credentials response, error: %s", err) + } + + err = credzRotateClient.Send(&credentialz.RotateAccountCredentialsRequest{ + Request: &credentialz.RotateAccountCredentialsRequest_Finalize{ + Finalize: request.GetFinalize(), + }, + }) + if err != nil { + t.Fatalf("failed sending credentialz rotate account credentials finalize request, error: %s", err) + } + + // brief sleep for finalize to get processed + time.Sleep(time.Second) +} + +func nokiaRole(t *testing.T) *gpb.SetRequest { + failRoleData, err := json.Marshal([]any{ + map[string]any{ + "services": []string{"cli"}, + "cli": map[string][]string{ + "deny-command-list": {"show version"}, }, - Replace: []*gpb.Update{ - { - Path: &gpb.Path{ - Elem: []*gpb.PathElem{ - {Name: "system"}, - {Name: "aaa"}, - {Name: "authorization"}, - {Name: "role", Key: map[string]string{"rolename": successRoleName}}, - }, - }, - Val: &gpb.TypedValue{ - Value: &gpb.TypedValue_JsonIetfVal{ - JsonIetfVal: successRoleData, - }, - }, - }, - { - Path: &gpb.Path{ - Elem: []*gpb.PathElem{ - {Name: "system"}, - {Name: "aaa"}, - {Name: "authorization"}, - {Name: "role", Key: map[string]string{"rolename": failRoleName}}, - }, - }, - Val: &gpb.TypedValue{ - Value: &gpb.TypedValue_JsonIetfVal{ - JsonIetfVal: failRoleData, - }, - }, - }, - { - Path: &gpb.Path{ - Elem: []*gpb.PathElem{ - {Name: "system"}, - {Name: "aaa"}, - {Name: "authentication"}, - {Name: "user", Key: map[string]string{"username": successUsername}}, - }, - }, - Val: &gpb.TypedValue{ - Value: &gpb.TypedValue_JsonIetfVal{ - JsonIetfVal: successUserData, - }, + }, + }) + if err != nil { + t.Fatalf("Error with json Marshal: %v", err) + } + + return &gpb.SetRequest{ + Prefix: &gpb.Path{ + Origin: "native", + }, + Replace: []*gpb.Update{ + { + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "system"}, + {Name: "aaa"}, + {Name: "authorization"}, + {Name: "role", Key: map[string]string{"rolename": failRoleName}}, }, }, - { - Path: &gpb.Path{ - Elem: []*gpb.PathElem{ - {Name: "system"}, - {Name: "aaa"}, - {Name: "authentication"}, - {Name: "user", Key: map[string]string{"username": failUsername}}, - }, - }, - Val: &gpb.TypedValue{ - Value: &gpb.TypedValue_JsonIetfVal{ - JsonIetfVal: failUserData, - }, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: failRoleData, }, }, }, - } - default: - t.Fatalf("Unsupported vendor %s for deviation 'deviation_native_users'", dut.Vendor()) - } - gnmiClient := dut.RawAPIs().GNMI(t) - if _, err := gnmiClient.Set(context.Background(), SetRequest); err != nil { - t.Fatalf("Unexpected error configuring User: %v", err) + }, } } func setupUsers(t *testing.T, dut *ondatra.DUTDevice) { - auth := &oc.System_Aaa_Authentication{} - auth.GetOrCreateUser(successUsername) - auth.GetOrCreateUser(failUsername) + var SetRequest *gpb.SetRequest - ondatragnmi.Update(t, dut, ondatragnmi.OC().System().Aaa().Authentication().Config(), auth) + //Create failure role in native + switch dut.Vendor() { + case ondatra.NOKIA: + SetRequest = nokiaRole(t) + } - if deviations.SetNativeUser(dut) { - // probably all vendors need to handle this since the user should have a role attached to - // it allowing us to login via ssh/console/whatever - createNativeRole(t, dut) + gnmiClient := dut.RawAPIs().GNMI(t) + if _, err := gnmiClient.Set(context.Background(), SetRequest); err != nil { + t.Fatalf("Unexpected error configuring role: %v", err) } + + //Configure users + auth := &oc.System_Aaa_Authentication{} + successUser := auth.GetOrCreateUser(successUsername) + successUser.SetRole(oc.AaaTypes_SYSTEM_DEFINED_ROLES_SYSTEM_ROLE_ADMIN) + failUser := auth.GetOrCreateUser(failUsername) + failUser.SetRole(oc.UnionString(failRoleName)) + ondatragnmi.Update(t, dut, ondatragnmi.OC().System().Aaa().Authentication().Config(), auth) + setupUserPassword(t, dut, successUsername, successPassword) + setupUserPassword(t, dut, failUsername, failPassword) } -func dialSSH(t *testing.T, username, password, addr string, port uint32) (net.Conn, io.Writer, io.Reader) { - tcpConn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", addr, port), 0) +func dialSSH(t *testing.T, username, password, target string) (net.Conn, io.Writer, io.Reader) { + tcpConn, err := net.DialTimeout("tcp", target, 0) if err != nil { t.Fatalf("got unexpected error dialing ssh tcp connection, error: %s", err) } cConn, chans, reqs, err := ssh.NewClientConn( tcpConn, - fmt.Sprintf("%s:%d", addr, port), + target, &ssh.ClientConfig{ User: username, Auth: []ssh.AuthMethod{ @@ -271,10 +246,10 @@ func dialSSH(t *testing.T, username, password, addr string, port uint32) (net.Co return tcpConn, w, r } -func sendCLICommand(t *testing.T, addr string, port uint32) []rpcRecord { +func sendCLICommand(t *testing.T, target string) []rpcRecord { var records []rpcRecord - tcpConn, w, _ := dialSSH(t, successUsername, successPassword, addr, port) + tcpConn, w, _ := dialSSH(t, successUsername, successPassword, target) defer func() { // give things a second to percolate then close the connection time.Sleep(3 * time.Second) @@ -296,41 +271,39 @@ func sendCLICommand(t *testing.T, addr string, port uint32) []rpcRecord { t.Fatalf("failed sending cli command, error: %s", err) } - addrParts := strings.Split(tcpConn.LocalAddr().String(), ":") - remoteAddr := addrParts[0] - remotePort, _ := strconv.Atoi(addrParts[1]) - - resolvedAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", addr, port)) + // remote from the perspective of the router + remoteAddr, err := net.ResolveTCPAddr("tcp", tcpConn.LocalAddr().String()) if err != nil { - t.Fatalf("failed resolving ssh destination addr, error: %s", err) + t.Fatalf("failed resolving ssh remote addr, error: %s", err) + } + localAddr, err := net.ResolveTCPAddr("tcp", target) + if err != nil { + t.Fatalf("failed resolving ssh local addr, error: %s", err) } - - addr = resolvedAddr.IP.String() records = append(records, rpcRecord{ startTime: startTime, doneTime: time.Now(), cmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, - localIp: addr, - localPort: port, - remoteIp: remoteAddr, - remotePort: uint32(remotePort), + localIp: localAddr.IP.String(), + localPort: uint32(localAddr.Port), + remoteIp: remoteAddr.IP.String(), + remotePort: uint32(remoteAddr.Port), succeeded: true, expectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, expectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, expectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, expectedAuthenCause: "authentication_method: local", expectedIdentity: successUsername, - expectedRole: successRoleName, }) return records } -func sendCLICommandFail(t *testing.T, addr string, port uint32) []rpcRecord { +func sendCLICommandFail(t *testing.T, target string) []rpcRecord { var records []rpcRecord - tcpConn, w, _ := dialSSH(t, failUsername, failPassword, addr, port) + tcpConn, w, _ := dialSSH(t, failUsername, failPassword, target) defer func() { // give things a second to percolate then close the connection time.Sleep(3 * time.Second) @@ -350,38 +323,36 @@ func sendCLICommandFail(t *testing.T, addr string, port uint32) []rpcRecord { t.Fatalf("failed sending cli command, error: %s", err) } - addrParts := strings.Split(tcpConn.LocalAddr().String(), ":") - remoteAddr := addrParts[0] - remotePort, _ := strconv.Atoi(addrParts[1]) - - resolvedAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", addr, port)) + // remote from the perspective of the router + remoteAddr, err := net.ResolveTCPAddr("tcp", tcpConn.LocalAddr().String()) if err != nil { - t.Fatalf("failed resolving ssh destination addr, error: %s", err) + t.Fatalf("failed resolving ssh remote addr, error: %s", err) + } + localAddr, err := net.ResolveTCPAddr("tcp", target) + if err != nil { + t.Fatalf("failed resolving ssh local addr, error: %s", err) } - - addr = resolvedAddr.IP.String() records = append(records, rpcRecord{ startTime: startTime, doneTime: time.Now(), cmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, - localIp: addr, - localPort: port, - remoteIp: remoteAddr, - remotePort: uint32(remotePort), + localIp: localAddr.IP.String(), + localPort: uint32(localAddr.Port), + remoteIp: remoteAddr.IP.String(), + remotePort: uint32(remoteAddr.Port), succeeded: true, expectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, expectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, expectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, expectedAuthenCause: "authentication_method: local", expectedIdentity: failUsername, - expectedRole: failRoleName, }) return records } -func sendShellCommand(t *testing.T, dut *ondatra.DUTDevice, addr string, port uint32) []rpcRecord { +func sendShellCommand(t *testing.T, dut *ondatra.DUTDevice, target string) []rpcRecord { var records []rpcRecord shellUsername := successUsername @@ -395,7 +366,7 @@ func sendShellCommand(t *testing.T, dut *ondatra.DUTDevice, addr string, port ui shellPassword = "NokiaSrl1!" } - tcpConn, w, _ := dialSSH(t, shellUsername, shellPassword, addr, port) + tcpConn, w, _ := dialSSH(t, shellUsername, shellPassword, target) defer func() { // give things a second to percolate then close the connection time.Sleep(3 * time.Second) @@ -415,25 +386,24 @@ func sendShellCommand(t *testing.T, dut *ondatra.DUTDevice, addr string, port ui t.Fatalf("failed sending cli command, error: %s", err) } - addrParts := strings.Split(tcpConn.LocalAddr().String(), ":") - remoteAddr := addrParts[0] - remotePort, _ := strconv.Atoi(addrParts[1]) - - resolvedAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", addr, port)) + // remote from the perspective of the router + remoteAddr, err := net.ResolveTCPAddr("tcp", tcpConn.LocalAddr().String()) if err != nil { - t.Fatalf("failed resolving ssh destination addr, error: %s", err) + t.Fatalf("failed resolving ssh remote addr, error: %s", err) + } + localAddr, err := net.ResolveTCPAddr("tcp", target) + if err != nil { + t.Fatalf("failed resolving ssh local addr, error: %s", err) } - - addr = resolvedAddr.IP.String() records = append(records, rpcRecord{ startTime: startTime, doneTime: time.Now(), cmdType: acctz.CommandService_CMD_SERVICE_TYPE_SHELL, - localIp: addr, - localPort: port, - remoteIp: remoteAddr, - remotePort: uint32(remotePort), + localIp: localAddr.IP.String(), + localPort: uint32(localAddr.Port), + remoteIp: remoteAddr.IP.String(), + remotePort: uint32(remoteAddr.Port), succeeded: true, expectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, expectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, @@ -465,6 +435,11 @@ func getDutAddr(t *testing.T, dut *ondatra.DUTDevice) string { return dutSSHService.GetOutsideIp() } +func prettyPrint(i interface{}) string { + s, _ := json.MarshalIndent(i, "", "\t") + return string(s) +} + func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { dut := ondatra.DUT(t, "dut") @@ -476,6 +451,10 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { // so... we get what we can get. addr := getDutAddr(t, dut) + // suppose ssh could be not 22 in some cases but don't think this is exposed by introspect + target := fmt.Sprintf("%s:%d", addr, sshPort) + t.Logf("Target for SSH service: %s", target) + var records []rpcRecord // put enough time between the test starting and any prior events so we can easily know where @@ -484,14 +463,13 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { startTime := time.Now() - // suppose ssh could be not 22 in some cases but don't think this is exposed by introspect - newRecords := sendCLICommand(t, addr, 22) + newRecords := sendCLICommand(t, target) records = append(records, newRecords...) - newRecords = sendCLICommandFail(t, addr, 22) + newRecords = sendCLICommandFail(t, target) records = append(records, newRecords...) - newRecords = sendShellCommand(t, dut, addr, 22) + newRecords = sendShellCommand(t, dut, target) records = append(records, newRecords...) // quick sleep to ensure all the records have been processed/ready for us @@ -564,7 +542,7 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { } if resp.record.GetHistoryIstruncated() { - t.Fatal("history is truncated but it shouldnt be") + t.Fatalf("history is truncated but it shouldn't be, Record Details: %s", prettyPrint(resp.record)) } if !resp.record.Timestamp.AsTime().After(startTime) { @@ -577,7 +555,7 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { if timestamp.UnixMilli() == lastTimestampUnixMillis { // this ensures that timestamps are actually changing for each record - t.Fatalf("timestamp is the same as the previous timestamp, this shouldnt be possible!") + t.Fatalf("timestamp is the same as the previous timestamp, this shouldn't be possible!, Record Details: %s", prettyPrint(resp.record)) } lastTimestampUnixMillis = timestamp.UnixMilli() @@ -592,32 +570,35 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { lastTaskID = currentTaskID - if records[recordIdx].startTime.Unix() > timestamp.Unix() { + // -2 for a little breathing room since things may not be perfectly synced up time-wise + if records[recordIdx].startTime.Unix()-2 > timestamp.Unix() { t.Fatalf( - "record timestamp is prior to rpc start time timestamp, rpc start timestamp %d, record timestamp %d", - records[recordIdx].startTime.Unix(), + "record timestamp is prior to rpc start time timestamp, rpc start timestamp %d, record timestamp %d, Record Details: %s", + records[recordIdx].startTime.Unix()-2, timestamp.Unix(), + prettyPrint(resp.record), ) } // done time (that we recorded when making the rpc) + 2 second for some breathing room if records[recordIdx].doneTime.Unix()+2 < timestamp.Unix() { t.Fatalf( - "record timestamp is after rpc end timestamp, rpc end timestamp %d, record timestamp %d", + "record timestamp is after rpc end timestamp, rpc end timestamp %d, record timestamp %d, Record Details: %s", records[recordIdx].doneTime.Unix()+2, timestamp.Unix(), + prettyPrint(resp.record), ) } cmdType := resp.record.GetCmdService().GetServiceType() if records[recordIdx].cmdType != cmdType { - t.Fatalf("service type not correct, got %q, want %q", cmdType, records[recordIdx].cmdType) + t.Fatalf("service type not correct, got %q, want %q, Record Details: %s", cmdType, records[recordIdx].cmdType, prettyPrint(resp.record)) } servicePath := resp.record.GetGrpcService().GetRpcName() if records[recordIdx].rpcPath != servicePath { - t.Fatalf("service path not correct, got %q, want %q", servicePath, records[recordIdx].rpcPath) + t.Fatalf("service path not correct, got %q, want %q, Record Details: %s", servicePath, records[recordIdx].rpcPath, prettyPrint(resp.record)) } channelID := resp.record.GetSessionInfo().GetChannelId() @@ -629,68 +610,62 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { // directly) if !records[recordIdx].succeeded { if channelID != "aaa_session_id: 0" { - t.Fatalf("auth was not successful for this record, but channel id was set, got %q", channelID) + t.Fatalf("auth was not successful for this record, but channel id was set, got %q, Record Details: %s", channelID, prettyPrint(resp.record)) } } // status sessionStatus := resp.record.GetSessionInfo().GetStatus() if records[recordIdx].expectedStatus != sessionStatus { - t.Fatalf("session status not correct, got %q, want %q", sessionStatus, records[recordIdx].expectedStatus) + t.Fatalf("session status not correct, got %q, want %q, Record Details: %s", sessionStatus, records[recordIdx].expectedStatus, prettyPrint(resp.record)) } // authen type authenType := resp.record.GetSessionInfo().GetAuthn().GetType() if records[recordIdx].expectedAuthenType != authenType { - t.Fatalf("authenType not correct, got %q, want %q", authenType, records[recordIdx].expectedAuthenType) + t.Fatalf("authenType not correct, got %q, want %q, Record Details: %s", authenType, records[recordIdx].expectedAuthenType, prettyPrint(resp.record)) } authenStatus := resp.record.GetSessionInfo().GetAuthn().GetStatus() if records[recordIdx].expectedAuthenStatus != authenStatus { - t.Fatalf("authenStatus not correct, got %q, want %q", authenStatus, records[recordIdx].expectedAuthenStatus) + t.Fatalf("authenStatus not correct, got %q, want %q, Record Details: %s", authenStatus, records[recordIdx].expectedAuthenStatus, prettyPrint(resp.record)) } authenCause := resp.record.GetSessionInfo().GetAuthn().GetCause() if records[recordIdx].expectedAuthenCause != authenCause { - t.Fatalf("authenCause not correct, got %q, want %q", authenCause, records[recordIdx].expectedAuthenCause) + t.Fatalf("authenCause not correct, got %q, want %q, Record Details: %s", authenCause, records[recordIdx].expectedAuthenCause, prettyPrint(resp.record)) } userIdentity := resp.record.GetSessionInfo().GetUser().GetIdentity() if records[recordIdx].expectedIdentity != userIdentity { - t.Fatalf("identity not correct, got %q, want %q", userIdentity, records[recordIdx].expectedIdentity) + t.Fatalf("identity not correct, got %q, want %q, Record Details: %s", userIdentity, records[recordIdx].expectedIdentity, prettyPrint(resp.record)) } if !records[recordIdx].succeeded { // not a successful rpc so don't need to check anything else recordIdx++ - continue } - role := resp.record.GetSessionInfo().GetUser().GetRole() - if records[recordIdx].expectedRole != role { - t.Fatalf("role not correct, got %q, want %q", role, records[recordIdx].expectedRole) - } - // verify the l4 bits align, this stuff is only set if auth is successful so do it down here localAddr := resp.record.GetSessionInfo().GetLocalAddress() if records[recordIdx].localIp != localAddr { - t.Fatalf("local address not correct, got %q, want %q", localAddr, records[recordIdx].localIp) + t.Fatalf("local address not correct, got %q, want %q, Record Details: %s", localAddr, records[recordIdx].localIp, prettyPrint(resp.record)) } localPort := resp.record.GetSessionInfo().GetLocalPort() if records[recordIdx].localPort != localPort { - t.Fatalf("local port not correct, got %d, want %d", localPort, records[recordIdx].localPort) + t.Fatalf("local port not correct, got %d, want %d, Record Details: %s", localPort, records[recordIdx].localPort, prettyPrint(resp.record)) } remoteAddr := resp.record.GetSessionInfo().GetRemoteAddress() if records[recordIdx].remoteIp != remoteAddr { - t.Fatalf("remote address not correct, got %q, want %q", remoteAddr, records[recordIdx].remoteIp) + t.Fatalf("remote address not correct, got %q, want %q, Record Details: %s", remoteAddr, records[recordIdx].remoteIp, prettyPrint(resp.record)) } remotePort := resp.record.GetSessionInfo().GetRemotePort() if records[recordIdx].remotePort != remotePort { - t.Fatalf("remote port not correct, got %d, want %d", remotePort, records[recordIdx].remotePort) + t.Fatalf("remote port not correct, got %d, want %d, Record Details: %s", remotePort, records[recordIdx].remotePort, prettyPrint(resp.record)) } recordIdx++ From 7df67d12742077097193bae62e65130cc144d8b4 Mon Sep 17 00:00:00 2001 From: "Dipesh Chauhan (Nokia)" Date: Fri, 20 Sep 2024 17:14:54 -0400 Subject: [PATCH 3/4] Refactor code "This code is a Contribution to the OpenConfig Feature Profiles project ("Work") made under the Google Software Grant and Corporate Contributor License Agreement ("CLA") and governed by the Apache License 2.0. No other rights or licenses in or to any of Nokia's intellectual property are granted for any other purpose. This code is provided on an "as is" basis without any warranties of any kind." --- .../record_subscribe_non_grpc_test.go | 628 ++---------- internal/security/acctz/acctz.go | 917 ++++++++++++++++++ 2 files changed, 1022 insertions(+), 523 deletions(-) create mode 100644 internal/security/acctz/acctz.go diff --git a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go index 852256ad212..c7677e05411 100644 --- a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go +++ b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go @@ -1,59 +1,35 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package record_subscribe_non_grpc_test import ( "context" "encoding/json" - "fmt" - "io" - "net" "testing" "time" - "github.com/openconfig/gnsi/credentialz" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/openconfig/featureprofiles/internal/fptest" - gpb "github.com/openconfig/gnmi/proto/gnmi" - "github.com/openconfig/gnsi/acctz" - tpb "github.com/openconfig/kne/proto/topo" + "github.com/openconfig/featureprofiles/internal/security/acctz" + acctzpb "github.com/openconfig/gnsi/acctz" "github.com/openconfig/ondatra" - "github.com/openconfig/ondatra/binding" - ondatragnmi "github.com/openconfig/ondatra/gnmi" - "github.com/openconfig/ondatra/gnmi/oc" - "golang.org/x/crypto/ssh" - "google.golang.org/protobuf/types/known/timestamppb" ) -const ( - successUsername = "acctztestuser" - successPassword = "verysecurepassword" - failUsername = "bilbo" - failPassword = "baggins" - failRoleName = "acctz-fp-test-fail" - command = "show version" - failCommand = "show version" - shellCommand = "uname -a" - sshPort = 22 -) - -type rpcRecord struct { - startTime time.Time - doneTime time.Time - cmdType acctz.CommandService_CmdServiceType - rpcPath string - localIp string - localPort uint32 - remoteIp string - remotePort uint32 - succeeded bool - expectedStatus acctz.SessionInfo_SessionStatus - expectedAuthenType acctz.AuthnDetail_AuthnType - expectedAuthenStatus acctz.AuthnDetail_AuthnStatus - expectedAuthenCause string - expectedIdentity string -} - type recordRequestResult struct { - record *acctz.RecordResponse + record *acctzpb.RecordResponse err error } @@ -61,380 +37,6 @@ func TestMain(m *testing.M) { fptest.RunTests(m) } -func setupUserPassword(t *testing.T, dut *ondatra.DUTDevice, username, password string) { - request := &credentialz.RotateAccountCredentialsRequest{ - Request: &credentialz.RotateAccountCredentialsRequest_Password{ - Password: &credentialz.PasswordRequest{ - Accounts: []*credentialz.PasswordRequest_Account{ - { - Account: username, - Password: &credentialz.PasswordRequest_Password{ - Value: &credentialz.PasswordRequest_Password_Plaintext{ - Plaintext: password, - }, - }, - Version: "v1.0", - CreatedOn: uint64(time.Now().Unix()), - }, - }, - }, - }, - } - - credzClient := dut.RawAPIs().GNSI(t).Credentialz() - - credzRotateClient, err := credzClient.RotateAccountCredentials(context.Background()) - if err != nil { - t.Fatalf("failed fetching credentialz rotate account credentials client, error: %s", err) - } - - err = credzRotateClient.Send(request) - if err != nil { - t.Fatalf("failed sending credentialz rotate account credentials request, error: %s", err) - } - - _, err = credzRotateClient.Recv() - if err != nil { - t.Fatalf("failed receiving credentialz rotate account credentials response, error: %s", err) - } - - err = credzRotateClient.Send(&credentialz.RotateAccountCredentialsRequest{ - Request: &credentialz.RotateAccountCredentialsRequest_Finalize{ - Finalize: request.GetFinalize(), - }, - }) - if err != nil { - t.Fatalf("failed sending credentialz rotate account credentials finalize request, error: %s", err) - } - - // brief sleep for finalize to get processed - time.Sleep(time.Second) -} - -func nokiaRole(t *testing.T) *gpb.SetRequest { - failRoleData, err := json.Marshal([]any{ - map[string]any{ - "services": []string{"cli"}, - "cli": map[string][]string{ - "deny-command-list": {"show version"}, - }, - }, - }) - if err != nil { - t.Fatalf("Error with json Marshal: %v", err) - } - - return &gpb.SetRequest{ - Prefix: &gpb.Path{ - Origin: "native", - }, - Replace: []*gpb.Update{ - { - Path: &gpb.Path{ - Elem: []*gpb.PathElem{ - {Name: "system"}, - {Name: "aaa"}, - {Name: "authorization"}, - {Name: "role", Key: map[string]string{"rolename": failRoleName}}, - }, - }, - Val: &gpb.TypedValue{ - Value: &gpb.TypedValue_JsonIetfVal{ - JsonIetfVal: failRoleData, - }, - }, - }, - }, - } -} - -func setupUsers(t *testing.T, dut *ondatra.DUTDevice) { - var SetRequest *gpb.SetRequest - - //Create failure role in native - switch dut.Vendor() { - case ondatra.NOKIA: - SetRequest = nokiaRole(t) - } - - gnmiClient := dut.RawAPIs().GNMI(t) - if _, err := gnmiClient.Set(context.Background(), SetRequest); err != nil { - t.Fatalf("Unexpected error configuring role: %v", err) - } - - //Configure users - auth := &oc.System_Aaa_Authentication{} - successUser := auth.GetOrCreateUser(successUsername) - successUser.SetRole(oc.AaaTypes_SYSTEM_DEFINED_ROLES_SYSTEM_ROLE_ADMIN) - failUser := auth.GetOrCreateUser(failUsername) - failUser.SetRole(oc.UnionString(failRoleName)) - ondatragnmi.Update(t, dut, ondatragnmi.OC().System().Aaa().Authentication().Config(), auth) - setupUserPassword(t, dut, successUsername, successPassword) - setupUserPassword(t, dut, failUsername, failPassword) -} - -func dialSSH(t *testing.T, username, password, target string) (net.Conn, io.Writer, io.Reader) { - tcpConn, err := net.DialTimeout("tcp", target, 0) - if err != nil { - t.Fatalf("got unexpected error dialing ssh tcp connection, error: %s", err) - } - - cConn, chans, reqs, err := ssh.NewClientConn( - tcpConn, - target, - &ssh.ClientConfig{ - User: username, - Auth: []ssh.AuthMethod{ - ssh.Password(password), - ssh.KeyboardInteractive( - func(user, instruction string, questions []string, echos []bool) ([]string, error) { - answers := make([]string, len(questions)) - for i := range answers { - answers[i] = password - } - - return answers, nil - }, - ), - }, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - }, - ) - if err != nil { - t.Fatalf("got unexpected error dialing ssh, error: %s", err) - } - - // stdin/stdout so we get a tty allocated - conn := ssh.NewClient(cConn, chans, reqs) - - sess, err := conn.NewSession() - if err != nil { - t.Fatalf("failed creating ssh session, error: %s", err) - } - - w, err := sess.StdinPipe() - if err != nil { - t.Fatal(err) - } - - r, err := sess.StdoutPipe() - if err != nil { - t.Fatal(err) - } - - term := ssh.TerminalModes{ - ssh.ECHO: 1, - ssh.TTY_OP_ISPEED: 115200, - ssh.TTY_OP_OSPEED: 115200, - } - - err = sess.RequestPty( - "xterm", - 255, - 80, - term, - ) - if err != nil { - t.Fatal(err) - } - - err = sess.Shell() - if err != nil { - t.Fatal(err) - } - - return tcpConn, w, r -} - -func sendCLICommand(t *testing.T, target string) []rpcRecord { - var records []rpcRecord - - tcpConn, w, _ := dialSSH(t, successUsername, successPassword, target) - defer func() { - // give things a second to percolate then close the connection - time.Sleep(3 * time.Second) - - err := tcpConn.Close() - if err != nil { - t.Logf("error closing tcp(ssh) connection, will ignore, error: %s", err) - } - }() - - startTime := time.Now() - - time.Sleep(time.Second) - - // this might not work for other vendors, so probably we can have a switch here and pass - // the writer to func per vendor if needed - _, err := w.Write([]byte(fmt.Sprintf("%s\n", command))) - if err != nil { - t.Fatalf("failed sending cli command, error: %s", err) - } - - // remote from the perspective of the router - remoteAddr, err := net.ResolveTCPAddr("tcp", tcpConn.LocalAddr().String()) - if err != nil { - t.Fatalf("failed resolving ssh remote addr, error: %s", err) - } - localAddr, err := net.ResolveTCPAddr("tcp", target) - if err != nil { - t.Fatalf("failed resolving ssh local addr, error: %s", err) - } - - records = append(records, rpcRecord{ - startTime: startTime, - doneTime: time.Now(), - cmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, - localIp: localAddr.IP.String(), - localPort: uint32(localAddr.Port), - remoteIp: remoteAddr.IP.String(), - remotePort: uint32(remoteAddr.Port), - succeeded: true, - expectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, - expectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - expectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, - expectedAuthenCause: "authentication_method: local", - expectedIdentity: successUsername, - }) - - return records -} - -func sendCLICommandFail(t *testing.T, target string) []rpcRecord { - var records []rpcRecord - - tcpConn, w, _ := dialSSH(t, failUsername, failPassword, target) - defer func() { - // give things a second to percolate then close the connection - time.Sleep(3 * time.Second) - - err := tcpConn.Close() - if err != nil { - t.Logf("error closing tcp(ssh) connection, will ignore, error: %s", err) - } - }() - - startTime := time.Now() - - time.Sleep(time.Second) - - _, err := w.Write([]byte(fmt.Sprintf("%s\n", failCommand))) - if err != nil { - t.Fatalf("failed sending cli command, error: %s", err) - } - - // remote from the perspective of the router - remoteAddr, err := net.ResolveTCPAddr("tcp", tcpConn.LocalAddr().String()) - if err != nil { - t.Fatalf("failed resolving ssh remote addr, error: %s", err) - } - localAddr, err := net.ResolveTCPAddr("tcp", target) - if err != nil { - t.Fatalf("failed resolving ssh local addr, error: %s", err) - } - - records = append(records, rpcRecord{ - startTime: startTime, - doneTime: time.Now(), - cmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, - localIp: localAddr.IP.String(), - localPort: uint32(localAddr.Port), - remoteIp: remoteAddr.IP.String(), - remotePort: uint32(remoteAddr.Port), - succeeded: true, - expectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, - expectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - expectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, - expectedAuthenCause: "authentication_method: local", - expectedIdentity: failUsername, - }) - - return records -} - -func sendShellCommand(t *testing.T, dut *ondatra.DUTDevice, target string) []rpcRecord { - var records []rpcRecord - - shellUsername := successUsername - shellPassword := successPassword - - switch dut.Vendor() { - case ondatra.NOKIA: - // assuming linuxadmin is present and ssh'ing directly via this user gets us to shell - // straight away so this is easy button to trigger a shell record - shellUsername = "linuxadmin" - shellPassword = "NokiaSrl1!" - } - - tcpConn, w, _ := dialSSH(t, shellUsername, shellPassword, target) - defer func() { - // give things a second to percolate then close the connection - time.Sleep(3 * time.Second) - - err := tcpConn.Close() - if err != nil { - t.Logf("error closing tcp(ssh) connection, will ignore, error: %s", err) - } - }() - - startTime := time.Now() - - // this might not work for other vendors, so probably we can have a switch here and pass - // the writer to func per vendor if needed - _, err := w.Write([]byte(fmt.Sprintf("%s\n", shellCommand))) - if err != nil { - t.Fatalf("failed sending cli command, error: %s", err) - } - - // remote from the perspective of the router - remoteAddr, err := net.ResolveTCPAddr("tcp", tcpConn.LocalAddr().String()) - if err != nil { - t.Fatalf("failed resolving ssh remote addr, error: %s", err) - } - localAddr, err := net.ResolveTCPAddr("tcp", target) - if err != nil { - t.Fatalf("failed resolving ssh local addr, error: %s", err) - } - - records = append(records, rpcRecord{ - startTime: startTime, - doneTime: time.Now(), - cmdType: acctz.CommandService_CMD_SERVICE_TYPE_SHELL, - localIp: localAddr.IP.String(), - localPort: uint32(localAddr.Port), - remoteIp: remoteAddr.IP.String(), - remotePort: uint32(remoteAddr.Port), - succeeded: true, - expectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, - expectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - expectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, - expectedAuthenCause: "", - expectedIdentity: shellUsername, - }) - - return records -} - -func getDutAddr(t *testing.T, dut *ondatra.DUTDevice) string { - var serviceDUT interface { - Service(string) (*tpb.Service, error) - } - - err := binding.DUTAs(dut.RawAPIs().BindingDUT(), &serviceDUT) - if err != nil { - t.Log("DUT does not support `Service` function, will attempt to use dut name field") - - return dut.Name() - } - - dutSSHService, err := serviceDUT.Service("ssh") - if err != nil { - t.Fatal(err) - } - - return dutSSHService.GetOutsideIp() -} - func prettyPrint(i interface{}) string { s, _ := json.MarshalIndent(i, "", "\t") return string(s) @@ -442,78 +44,59 @@ func prettyPrint(i interface{}) string { func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { dut := ondatra.DUT(t, "dut") + acctz.SetupSSHUsers(t, dut) + var records []acctz.Record - setupUsers(t, dut) - - // https://github.com/openconfig/featureprofiles/issues/2637 - // basically, just waiting to see what the "best"/"preferred" way is to get the v4/v6 of the - // dut -- for now we use this hacky work around because ssh isn't exposed in introspection anyway - // so... we get what we can get. - addr := getDutAddr(t, dut) - - // suppose ssh could be not 22 in some cases but don't think this is exposed by introspect - target := fmt.Sprintf("%s:%d", addr, sshPort) - t.Logf("Target for SSH service: %s", target) - - var records []rpcRecord - - // put enough time between the test starting and any prior events so we can easily know where - // our records start + // Put enough time between the test starting and any prior events so we can easily know where + // our records start. time.Sleep(5 * time.Second) startTime := time.Now() - - newRecords := sendCLICommand(t, target) + newRecords := acctz.SendSuccessCliCommand(t, dut) records = append(records, newRecords...) - - newRecords = sendCLICommandFail(t, target) + newRecords = acctz.SendFailCliCommand(t, dut) records = append(records, newRecords...) - - newRecords = sendShellCommand(t, dut, target) + newRecords = acctz.SendShellCommand(t, dut) records = append(records, newRecords...) - // quick sleep to ensure all the records have been processed/ready for us + // Quick sleep to ensure all the records have been processed/ready for us. time.Sleep(5 * time.Second) - // get gnsi record subscribe client + // Get gNSI record subscribe client. acctzClient := dut.RawAPIs().GNSI(t).Acctz() acctzSubClient, err := acctzClient.RecordSubscribe(context.Background()) if err != nil { - t.Fatalf("failed getting accountz record subscribe client, error: %s", err) + t.Fatalf("Failed getting accountz record subscribe client, error: %s", err) } - // this will have to move up to RecordSubscribe call after this is brought into fp/ondatra stuff + // This will have to move up to RecordSubscribe call after this is brought into FP/Ondatra. // https://github.com/openconfig/gnsi/pull/149/files - err = acctzSubClient.Send(&acctz.RecordRequest{ + err = acctzSubClient.Send(&acctzpb.RecordRequest{ Timestamp: ×tamppb.Timestamp{ Seconds: 0, Nanos: 0, }, }) if err != nil { - t.Fatalf("failed sending accountz record request, error: %s", err) + t.Fatalf("Failed sending accountz record request, error: %s", err) } var recordIdx int - var lastTimestampUnixMillis int64 var lastTaskID string for { if recordIdx >= len(records) { - t.Log("out of records to process...") - + t.Log("Out of records to process...") break } r := make(chan recordRequestResult) go func(r chan recordRequestResult) { - var response *acctz.RecordResponse - + var response *acctzpb.RecordResponse response, err = acctzSubClient.Recv() - r <- recordRequestResult{ record: response, err: err, @@ -532,146 +115,145 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { } if done { - t.Log("done receiving records...") - + t.Log("Done receiving records...") break } if resp.err != nil { - t.Fatalf("failed receiving record response, error: %s", resp.err) + t.Fatalf("Failed receiving record response, error: %s", resp.err) } if resp.record.GetHistoryIstruncated() { - t.Fatalf("history is truncated but it shouldn't be, Record Details: %s", prettyPrint(resp.record)) + t.Fatalf("History is truncated but it shouldn't be, Record Details: %s", prettyPrint(resp.record)) } if !resp.record.Timestamp.AsTime().After(startTime) { - // skipping record, was before test start time + // Skipping record if it happened before test start time. continue } - // check that the timestamp for the record is between our start/stop times for our rpc - timestamp := resp.record.Timestamp.AsTime() - - if timestamp.UnixMilli() == lastTimestampUnixMillis { - // this ensures that timestamps are actually changing for each record - t.Fatalf("timestamp is the same as the previous timestamp, this shouldn't be possible!, Record Details: %s", prettyPrint(resp.record)) + if resp.record.GetGrpcService().GetServiceType() != acctzpb.GrpcService_GRPC_SERVICE_TYPE_UNSPECIFIED { + // Skipping gRPC records (if any). + continue } - lastTimestampUnixMillis = timestamp.UnixMilli() - - // some task ids may be tracked multiple times (for start/stop accounting). if we see two in - // a row that are the same task we know this is what's up and we can skip this record and - // continue + // Some task ids may be tracked multiple times (for start/stop accounting). If we see two in + // a row that are the same task, we can skip this record and continue. currentTaskID := resp.record.TaskIds[0] if currentTaskID == lastTaskID { continue } - lastTaskID = currentTaskID - // -2 for a little breathing room since things may not be perfectly synced up time-wise - if records[recordIdx].startTime.Unix()-2 > timestamp.Unix() { + // Check that the timestamp for the record is between our start/stop times for our cmd. + timestamp := resp.record.Timestamp.AsTime() + if timestamp.UnixMilli() == lastTimestampUnixMillis { + // This ensures that timestamps are actually changing for each record. + t.Fatalf("Timestamp is the same as the previous timestamp, this shouldn't be possible!, Record Details: %s", prettyPrint(resp.record)) + } + lastTimestampUnixMillis = timestamp.UnixMilli() + + // -2 for a little breathing room since things may not be perfectly synced up time-wise. + if records[recordIdx].StartTime.Unix()-2 > timestamp.Unix() { t.Fatalf( - "record timestamp is prior to rpc start time timestamp, rpc start timestamp %d, record timestamp %d, Record Details: %s", - records[recordIdx].startTime.Unix()-2, + "Record timestamp is prior to cmd start time timestamp, cmd start timestamp %d, record timestamp %d, Record Details: %s", + records[recordIdx].StartTime.Unix()-2, timestamp.Unix(), prettyPrint(resp.record), ) } - // done time (that we recorded when making the rpc) + 2 second for some breathing room - if records[recordIdx].doneTime.Unix()+2 < timestamp.Unix() { + // Done time (that we recorded when making the cmd) + 2 second for some breathing room. + if records[recordIdx].DoneTime.Unix()+2 < timestamp.Unix() { t.Fatalf( - "record timestamp is after rpc end timestamp, rpc end timestamp %d, record timestamp %d, Record Details: %s", - records[recordIdx].doneTime.Unix()+2, + "Record timestamp is after cmd end timestamp, cmd end timestamp %d, record timestamp %d, Record Details: %s", + records[recordIdx].DoneTime.Unix()+2, timestamp.Unix(), prettyPrint(resp.record), ) } cmdType := resp.record.GetCmdService().GetServiceType() - - if records[recordIdx].cmdType != cmdType { - t.Fatalf("service type not correct, got %q, want %q, Record Details: %s", cmdType, records[recordIdx].cmdType, prettyPrint(resp.record)) + if records[recordIdx].CmdType != cmdType { + t.Fatalf("Service type not correct, got %q, want %q, Record Details: %s", cmdType, records[recordIdx].CmdType, prettyPrint(resp.record)) } - servicePath := resp.record.GetGrpcService().GetRpcName() - if records[recordIdx].rpcPath != servicePath { - t.Fatalf("service path not correct, got %q, want %q, Record Details: %s", servicePath, records[recordIdx].rpcPath, prettyPrint(resp.record)) + cmd := resp.record.GetCmdService().GetCmd() + if records[recordIdx].Cmd != cmd { + t.Fatalf("Command not correct, got %q, want %q, Record Details: %s", cmd, records[recordIdx].Cmd, prettyPrint(resp.record)) } - channelID := resp.record.GetSessionInfo().GetChannelId() - - // this channel check maybe should just go away entirely -- see: + // This channel check maybe should just go away entirely -- see: // https://github.com/openconfig/gnsi/issues/98 - // in case of nokia this is being set to the aaa session id just to have some hopefully + // In case of Nokia this is being set to the aaa session id just to have some hopefully // useful info in this field to identify a "session" (even if it isn't necessarily ssh/grpc - // directly) - if !records[recordIdx].succeeded { + // directly). + channelID := resp.record.GetSessionInfo().GetChannelId() + if !records[recordIdx].Succeeded { if channelID != "aaa_session_id: 0" { - t.Fatalf("auth was not successful for this record, but channel id was set, got %q, Record Details: %s", channelID, prettyPrint(resp.record)) + t.Fatalf("Auth was not successful for this record, but channel id was set, got %q, Record Details: %s", channelID, prettyPrint(resp.record)) } } - // status + // Tty only set for ssh records. + tty := resp.record.GetSessionInfo().GetTty() + if tty == "" { + t.Fatalf("Should have tty allocated but not set, Record Details: %s", prettyPrint(resp.record)) + } + sessionStatus := resp.record.GetSessionInfo().GetStatus() - if records[recordIdx].expectedStatus != sessionStatus { - t.Fatalf("session status not correct, got %q, want %q, Record Details: %s", sessionStatus, records[recordIdx].expectedStatus, prettyPrint(resp.record)) + if records[recordIdx].ExpectedStatus != sessionStatus { + t.Fatalf("Session status not correct, got %q, want %q, Record Details: %s", sessionStatus, records[recordIdx].ExpectedStatus, prettyPrint(resp.record)) } - // authen type authenType := resp.record.GetSessionInfo().GetAuthn().GetType() - if records[recordIdx].expectedAuthenType != authenType { - t.Fatalf("authenType not correct, got %q, want %q, Record Details: %s", authenType, records[recordIdx].expectedAuthenType, prettyPrint(resp.record)) + if records[recordIdx].ExpectedAuthenType != authenType { + t.Fatalf("AuthenType not correct, got %q, want %q, Record Details: %s", authenType, records[recordIdx].ExpectedAuthenType, prettyPrint(resp.record)) } authenStatus := resp.record.GetSessionInfo().GetAuthn().GetStatus() - if records[recordIdx].expectedAuthenStatus != authenStatus { - t.Fatalf("authenStatus not correct, got %q, want %q, Record Details: %s", authenStatus, records[recordIdx].expectedAuthenStatus, prettyPrint(resp.record)) + if records[recordIdx].ExpectedAuthenStatus != authenStatus { + t.Fatalf("AuthenStatus not correct, got %q, want %q, Record Details: %s", authenStatus, records[recordIdx].ExpectedAuthenStatus, prettyPrint(resp.record)) } authenCause := resp.record.GetSessionInfo().GetAuthn().GetCause() - if records[recordIdx].expectedAuthenCause != authenCause { - t.Fatalf("authenCause not correct, got %q, want %q, Record Details: %s", authenCause, records[recordIdx].expectedAuthenCause, prettyPrint(resp.record)) + if records[recordIdx].ExpectedAuthenCause != authenCause { + t.Fatalf("AuthenCause not correct, got %q, want %q, Record Details: %s", authenCause, records[recordIdx].ExpectedAuthenCause, prettyPrint(resp.record)) } userIdentity := resp.record.GetSessionInfo().GetUser().GetIdentity() - if records[recordIdx].expectedIdentity != userIdentity { - t.Fatalf("identity not correct, got %q, want %q, Record Details: %s", userIdentity, records[recordIdx].expectedIdentity, prettyPrint(resp.record)) - } - - if !records[recordIdx].succeeded { - // not a successful rpc so don't need to check anything else - recordIdx++ - continue + if records[recordIdx].ExpectedIdentity != userIdentity { + t.Fatalf("Identity not correct, got %q, want %q, Record Details: %s", userIdentity, records[recordIdx].ExpectedIdentity, prettyPrint(resp.record)) } - // verify the l4 bits align, this stuff is only set if auth is successful so do it down here - localAddr := resp.record.GetSessionInfo().GetLocalAddress() - if records[recordIdx].localIp != localAddr { - t.Fatalf("local address not correct, got %q, want %q, Record Details: %s", localAddr, records[recordIdx].localIp, prettyPrint(resp.record)) - } + if records[recordIdx].Succeeded { + // Verify the l4 bits align, this is only set if auth is successful so do it down here. + localAddr := resp.record.GetSessionInfo().GetLocalAddress() + if records[recordIdx].LocalIP != localAddr { + t.Fatalf("Local address not correct, got %q, want %q, Record Details: %s", localAddr, records[recordIdx].LocalIP, prettyPrint(resp.record)) + } - localPort := resp.record.GetSessionInfo().GetLocalPort() - if records[recordIdx].localPort != localPort { - t.Fatalf("local port not correct, got %d, want %d, Record Details: %s", localPort, records[recordIdx].localPort, prettyPrint(resp.record)) - } + localPort := resp.record.GetSessionInfo().GetLocalPort() + if records[recordIdx].LocalPort != localPort { + t.Fatalf("Local port not correct, got %d, want %d, Record Details: %s", localPort, records[recordIdx].LocalPort, prettyPrint(resp.record)) + } - remoteAddr := resp.record.GetSessionInfo().GetRemoteAddress() - if records[recordIdx].remoteIp != remoteAddr { - t.Fatalf("remote address not correct, got %q, want %q, Record Details: %s", remoteAddr, records[recordIdx].remoteIp, prettyPrint(resp.record)) - } + remoteAddr := resp.record.GetSessionInfo().GetRemoteAddress() + if records[recordIdx].RemoteIP != remoteAddr { + t.Fatalf("Remote address not correct, got %q, want %q, Record Details: %s", remoteAddr, records[recordIdx].RemoteIP, prettyPrint(resp.record)) + } - remotePort := resp.record.GetSessionInfo().GetRemotePort() - if records[recordIdx].remotePort != remotePort { - t.Fatalf("remote port not correct, got %d, want %d, Record Details: %s", remotePort, records[recordIdx].remotePort, prettyPrint(resp.record)) + remotePort := resp.record.GetSessionInfo().GetRemotePort() + if records[recordIdx].RemotePort != remotePort { + t.Fatalf("Remote port not correct, got %d, want %d, Record Details: %s", remotePort, records[recordIdx].RemotePort, prettyPrint(resp.record)) + } } + t.Logf("Processed Record: %s", prettyPrint(resp.record)) recordIdx++ } if recordIdx != len(records) { - t.Fatal("did not process all records") + t.Fatal("Did not process all records.") } } diff --git a/internal/security/acctz/acctz.go b/internal/security/acctz/acctz.go new file mode 100644 index 00000000000..b45cc4a270c --- /dev/null +++ b/internal/security/acctz/acctz.go @@ -0,0 +1,917 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package acctz provides helper APIs to simplify writing acctz test cases. +package acctz + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "strconv" + "testing" + "time" + + "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/gnoi/system" + "github.com/openconfig/gnsi/acctz" + authzpb "github.com/openconfig/gnsi/authz" + "github.com/openconfig/gnsi/credentialz" + gribi "github.com/openconfig/gribi/v1/proto/service" + tpb "github.com/openconfig/kne/proto/topo" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/binding" + "github.com/openconfig/ondatra/binding/introspect" + ondatragnmi "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + p4pb "github.com/p4lang/p4runtime/go/p4/v1" + "golang.org/x/crypto/ssh" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/anypb" +) + +const ( + successUsername = "acctztestuser" + successPassword = "verysecurepassword" + failUsername = "bilbo" + failPassword = "baggins" + failRoleName = "acctz-fp-test-fail" + successCliCommand = "show version" + failCliCommand = "show version" + shellCommand = "uname -a" + gnmiCapabilitiesPath = "/gnmi.gNMI/Capabilities" + gnoiPingPath = "/gnoi.system.System/Ping" + gnsiGetPath = "/gnsi.authz.v1.Authz/Get" + defaultSSHPort = 22 +) + +var gRPCClientAddr net.Addr + +// Record represents the structure for an acctz record. +type Record struct { + StartTime time.Time + DoneTime time.Time + CmdType acctz.CommandService_CmdServiceType + Cmd string + RPCType acctz.GrpcService_GrpcServiceType + RPCPath string + RPCPayload string + LocalIP string + LocalPort uint32 + RemoteIP string + RemotePort uint32 + Succeeded bool + ExpectedStatus acctz.SessionInfo_SessionStatus + ExpectedAuthenType acctz.AuthnDetail_AuthnType + ExpectedAuthenStatus acctz.AuthnDetail_AuthnStatus + ExpectedAuthenCause string + ExpectedIdentity string +} + +func setupUserPassword(t *testing.T, dut *ondatra.DUTDevice, username, password string) { + request := &credentialz.RotateAccountCredentialsRequest{ + Request: &credentialz.RotateAccountCredentialsRequest_Password{ + Password: &credentialz.PasswordRequest{ + Accounts: []*credentialz.PasswordRequest_Account{ + { + Account: username, + Password: &credentialz.PasswordRequest_Password{ + Value: &credentialz.PasswordRequest_Password_Plaintext{ + Plaintext: password, + }, + }, + Version: "v1.0", + CreatedOn: uint64(time.Now().Unix()), + }, + }, + }, + }, + } + + credzClient := dut.RawAPIs().GNSI(t).Credentialz() + credzRotateClient, err := credzClient.RotateAccountCredentials(context.Background()) + if err != nil { + t.Fatalf("Failed fetching credentialz rotate account credentials client, error: %s", err) + } + err = credzRotateClient.Send(request) + if err != nil { + t.Fatalf("Failed sending credentialz rotate account credentials request, error: %s", err) + } + _, err = credzRotateClient.Recv() + if err != nil { + t.Fatalf("Failed receiving credentialz rotate account credentials response, error: %s", err) + } + err = credzRotateClient.Send(&credentialz.RotateAccountCredentialsRequest{ + Request: &credentialz.RotateAccountCredentialsRequest_Finalize{ + Finalize: request.GetFinalize(), + }, + }) + if err != nil { + t.Fatalf("Failed sending credentialz rotate account credentials finalize request, error: %s", err) + } + + // Brief sleep for finalize to get processed. + time.Sleep(time.Second) +} + +func nokiaFailCliRole(t *testing.T) *gnmi.SetRequest { + failRoleData, err := json.Marshal([]any{ + map[string]any{ + "services": []string{"cli"}, + "cli": map[string][]string{ + "deny-command-list": {failCliCommand}, + }, + }, + }) + if err != nil { + t.Fatalf("Error with json marshal: %v", err) + } + + return &gnmi.SetRequest{ + Prefix: &gnmi.Path{ + Origin: "native", + }, + Replace: []*gnmi.Update{ + { + Path: &gnmi.Path{ + Elem: []*gnmi.PathElem{ + {Name: "system"}, + {Name: "aaa"}, + {Name: "authorization"}, + {Name: "role", Key: map[string]string{"rolename": failRoleName}}, + }, + }, + Val: &gnmi.TypedValue{ + Value: &gnmi.TypedValue_JsonIetfVal{ + JsonIetfVal: failRoleData, + }, + }, + }, + }, + } +} + +// SetupGrpcUsers Setup users for grpc-based acctz tests. +func SetupGrpcUsers(t *testing.T, dut *ondatra.DUTDevice) { + auth := &oc.System_Aaa_Authentication{} + successUser := auth.GetOrCreateUser(successUsername) + successUser.SetRole(oc.AaaTypes_SYSTEM_DEFINED_ROLES_SYSTEM_ROLE_ADMIN) + auth.GetOrCreateUser(failUsername) + ondatragnmi.Update(t, dut, ondatragnmi.OC().System().Aaa().Authentication().Config(), auth) + setupUserPassword(t, dut, successUsername, successPassword) +} + +// SetupSSHUsers Setup users for ssh-based acctz tests. +func SetupSSHUsers(t *testing.T, dut *ondatra.DUTDevice) { + var SetRequest *gnmi.SetRequest + + // Create failure cli role in native. + switch dut.Vendor() { + case ondatra.NOKIA: + SetRequest = nokiaFailCliRole(t) + } + + gnmiClient := dut.RawAPIs().GNMI(t) + if _, err := gnmiClient.Set(context.Background(), SetRequest); err != nil { + t.Fatalf("Unexpected error configuring role: %v", err) + } + + // Configure users. + auth := &oc.System_Aaa_Authentication{} + successUser := auth.GetOrCreateUser(successUsername) + successUser.SetRole(oc.AaaTypes_SYSTEM_DEFINED_ROLES_SYSTEM_ROLE_ADMIN) + failUser := auth.GetOrCreateUser(failUsername) + failUser.SetRole(oc.UnionString(failRoleName)) + ondatragnmi.Update(t, dut, ondatragnmi.OC().System().Aaa().Authentication().Config(), auth) + setupUserPassword(t, dut, successUsername, successPassword) + setupUserPassword(t, dut, failUsername, failPassword) +} + +func getGrpcTarget(t *testing.T, dut *ondatra.DUTDevice, service introspect.Service) string { + dialTarget := introspect.DUTDialer(t, dut, service).DialTarget + resolvedTarget, err := net.ResolveTCPAddr("tcp", dialTarget) + if err != nil { + t.Fatalf("Failed resolving %s target %s", service, dialTarget) + } + t.Logf("Target for %s service: %s", service, resolvedTarget) + return resolvedTarget.String() +} + +func getSSHTarget(t *testing.T, dut *ondatra.DUTDevice) string { + var serviceDUT interface { + Service(string) (*tpb.Service, error) + } + + var target string + err := binding.DUTAs(dut.RawAPIs().BindingDUT(), &serviceDUT) + if err != nil { + t.Log("DUT does not support `Service` function, will attempt to resolve dut name field.") + + // Suppose ssh could be not 22 in some cases but don't think this is exposed by introspect. + dialTarget := fmt.Sprintf("%s:%d", dut.Name(), defaultSSHPort) + resolvedTarget, err := net.ResolveTCPAddr("tcp", dialTarget) + if err != nil { + t.Fatalf("Failed resolving ssh target %s", dialTarget) + } + target = resolvedTarget.String() + } else { + dutSSHService, err := serviceDUT.Service("ssh") + if err != nil { + t.Fatal(err) + } + target = fmt.Sprintf("%s:%d", dutSSHService.GetOutsideIp(), defaultSSHPort) + } + + t.Logf("Target for ssh service: %s", target) + return target +} + +func dialGrpc(t *testing.T, target string) *grpc.ClientConn { + conn, err := grpc.NewClient( + target, + grpc.WithTransportCredentials( + credentials.NewTLS( + &tls.Config{ + InsecureSkipVerify: true, + }, + ), + ), + grpc.WithContextDialer(func(ctx context.Context, a string) (net.Conn, error) { + dst, err := net.ResolveTCPAddr("tcp", a) + if err != nil { + return nil, err + } + c, err := net.DialTCP("tcp", nil, dst) + if err != nil { + return nil, err + } + gRPCClientAddr = c.LocalAddr() + return c, err + })) + if err != nil { + t.Fatalf("Got unexpected error dialing gRPC target %q, error: %v", target, err) + } + + return conn +} + +func dialSSH(t *testing.T, username, password, target string) (*ssh.Client, io.WriteCloser) { + conn, err := ssh.Dial( + "tcp", + target, + &ssh.ClientConfig{ + User: username, + Auth: []ssh.AuthMethod{ + ssh.Password(password), + ssh.KeyboardInteractive( + func(user, instruction string, questions []string, echos []bool) ([]string, error) { + answers := make([]string, len(questions)) + for i := range answers { + answers[i] = password + } + return answers, nil + }, + ), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + }) + if err != nil { + t.Fatalf("Got unexpected error dialing ssh target %s, error: %v", target, err) + } + + sess, err := conn.NewSession() + if err != nil { + t.Fatalf("Failed creating ssh session, error: %s", err) + } + + w, err := sess.StdinPipe() + if err != nil { + t.Fatal(err) + } + + term := ssh.TerminalModes{ + ssh.ECHO: 0, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, + } + + err = sess.RequestPty( + "xterm", + 40, + 80, + term, + ) + if err != nil { + t.Fatal(err) + } + + err = sess.Shell() + if err != nil { + t.Fatal(err) + } + + return conn, w +} + +func getHostPortInfo(t *testing.T, address string) (string, uint32) { + ip, port, err := net.SplitHostPort(address) + if err != nil { + t.Fatal(err) + } + portNumber, err := strconv.Atoi(port) + if err != nil { + t.Fatal(err) + } + return ip, uint32(portNumber) +} + +// SendGnmiRPCs Setup gNMI test RPCs (successful and failed) to be used in the acctz client tests. +func SendGnmiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { + // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the + // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection + // but that won't get us v4 and v6, it will just get us whatever is configured in binding, + // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. + target := getGrpcTarget(t, dut, introspect.GNMI) + + var records []Record + grpcConn := dialGrpc(t, target) + gnmiClient := gnmi.NewGNMIClient(grpcConn) + ctx := context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) + startTime := time.Now() + + // Send an unsuccessful gNMI capabilities request (bad creds in context). + _, err := gnmiClient.Capabilities(ctx, &gnmi.CapabilityRequest{}) + if err != nil { + t.Logf("Got expected error fetching capabilities with bad creds, error: %s", err) + } else { + t.Fatal("Did not get expected error fetching capabilities with bad creds.") + } + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNMI, + RPCPath: gnmiCapabilitiesPath, + Succeeded: false, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + ExpectedIdentity: failUsername, + }) + + // Send a successful gNMI capabilities request. + ctx = context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", successUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", successPassword) + req := &gnmi.CapabilityRequest{} + payload, err := anypb.New(req) + if err != nil { + t.Fatal("Failed creating anypb payload.") + } + startTime = time.Now() + _, err = gnmiClient.Capabilities(ctx, req) + if err != nil { + t.Fatalf("Error fetching capabilities, error: %s", err) + } + + // Remote from the perspective of the router. + remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) + localIP, localPort := getHostPortInfo(t, target) + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNMI, + RPCPath: gnmiCapabilitiesPath, + RPCPayload: payload.String(), + LocalIP: localIP, + LocalPort: localPort, + RemoteIP: remoteIP, + RemotePort: remotePort, + Succeeded: true, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, + ExpectedAuthenCause: "authentication_method: local", + ExpectedIdentity: successUsername, + }) + + return records +} + +// SendGnoiRPCs Setup gNOI test RPCs (successful and failed) to be used in the acctz client tests. +func SendGnoiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { + // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the + // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection + // but that won't get us v4 and v6, it will just get us whatever is configured in binding, + // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. + target := getGrpcTarget(t, dut, introspect.GNOI) + + var records []Record + grpcConn := dialGrpc(t, target) + gnoiSystemClient := system.NewSystemClient(grpcConn) + ctx := context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) + startTime := time.Now() + + // Send an unsuccessful gNOI system time request (bad creds in context), we don't + // care about receiving on it, just want to make the request. + gnoiSystemPingClient, err := gnoiSystemClient.Ping(ctx, &system.PingRequest{ + Destination: "127.0.0.1", + Count: 1, + }) + if err != nil { + t.Fatalf("Got unexpected error getting gnoi system time client, error: %s", err) + } + + _, err = gnoiSystemPingClient.Recv() + if err != nil { + t.Logf("Got expected error getting gnoi system time with bad creds, error: %s", err) + } + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNOI, + RPCPath: gnoiPingPath, + Succeeded: false, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + ExpectedIdentity: failUsername, + }) + + // Send a successful gNOI ping request. + ctx = context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", successUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", successPassword) + req := &system.PingRequest{ + Destination: "127.0.0.1", + Count: 1, + } + payload, err := anypb.New(req) + if err != nil { + t.Fatal("Failed creating anypb payload.") + } + startTime = time.Now() + gnoiSystemPingClient, err = gnoiSystemClient.Ping(ctx, req) + if err != nil { + t.Fatalf("Error fetching gnoi system time, error: %s", err) + } + _, err = gnoiSystemPingClient.Recv() + if err != nil { + t.Fatalf("Got unexpected error getting gnoi system time, error: %s", err) + } + + // Remote from the perspective of the router. + remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) + localIP, localPort := getHostPortInfo(t, target) + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNOI, + RPCPath: gnoiPingPath, + RPCPayload: payload.String(), + LocalIP: localIP, + LocalPort: localPort, + RemoteIP: remoteIP, + RemotePort: remotePort, + Succeeded: true, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, + ExpectedAuthenCause: "authentication_method: local", + ExpectedIdentity: successUsername, + }) + + return records +} + +// SendGnsiRPCs Setup gNSI test RPCs (successful and failed) to be used in the acctz client tests. +func SendGnsiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { + // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the + // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection + // but that won't get us v4 and v6, it will just get us whatever is configured in binding, + // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. + target := getGrpcTarget(t, dut, introspect.GNSI) + + var records []Record + grpcConn := dialGrpc(t, target) + authzClient := authzpb.NewAuthzClient(grpcConn) + ctx := context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) + startTime := time.Now() + + // Send an unsuccessful gNSI authz get request (bad creds in context), we don't + // care about receiving on it, just want to make the request. + _, err := authzClient.Get(ctx, &authzpb.GetRequest{}) + if err != nil { + t.Logf("Got expected error fetching authz policy with bad creds, error: %s", err) + } else { + t.Fatal("Did not get expected error fetching authz policy with bad creds.") + } + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNSI, + RPCPath: gnsiGetPath, + Succeeded: false, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + ExpectedIdentity: failUsername, + }) + + // Send a successful gNSI authz get request. + ctx = context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", successUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", successPassword) + req := &authzpb.GetRequest{} + payload, err := anypb.New(req) + if err != nil { + t.Fatal("Failed creating anypb payload.") + } + startTime = time.Now() + _, err = authzClient.Get(ctx, &authzpb.GetRequest{}) + if err != nil { + t.Fatalf("Error fetching authz policy, error: %s", err) + } + + // Remote from the perspective of the router. + remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) + localIP, localPort := getHostPortInfo(t, target) + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNSI, + RPCPath: gnsiGetPath, + RPCPayload: payload.String(), + LocalIP: localIP, + LocalPort: localPort, + RemoteIP: remoteIP, + RemotePort: remotePort, + Succeeded: true, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, + ExpectedAuthenCause: "authentication_method: local", + ExpectedIdentity: successUsername, + }) + + return records +} + +// SendGribiRPCs Setup gRIBI test RPCs (successful and failed) to be used in the acctz client tests. +func SendGribiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { + // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the + // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection + // but that won't get us v4 and v6, it will just get us whatever is configured in binding, + // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. + target := getGrpcTarget(t, dut, introspect.GRIBI) + + var records []Record + grpcConn := dialGrpc(t, target) + gribiClient := gribi.NewGRIBIClient(grpcConn) + ctx := context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) + startTime := time.Now() + + // Send an unsuccessful gRIBI get request (bad creds in context), we don't + // care about receiving on it, just want to make the request. + gribiGetClient, err := gribiClient.Get( + ctx, + &gribi.GetRequest{ + NetworkInstance: &gribi.GetRequest_All{}, + Aft: gribi.AFTType_IPV4, + }, + ) + if err != nil { + t.Fatalf("Got unexpected error during gribi get request, error: %s", err) + } + _, err = gribiGetClient.Recv() + if err != nil { + t.Logf("Got expected error during gribi recv request, error: %s", err) + } + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GRIBI, + RPCPath: "/gribi.gRIBI/Get", + RPCPayload: "", + Succeeded: false, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + ExpectedIdentity: failUsername, + }) + + // Send a successful gRIBI get request. + ctx = context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", successUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", successPassword) + req := &gribi.GetRequest{ + NetworkInstance: &gribi.GetRequest_All{}, + Aft: gribi.AFTType_IPV4, + } + payload, err := anypb.New(req) + if err != nil { + t.Fatal("Failed creating anypb payload.") + } + startTime = time.Now() + gribiGetClient, err = gribiClient.Get(ctx, req) + if err != nil { + t.Fatalf("Got unexpected error during gribi get request, error: %s", err) + } + _, err = gribiGetClient.Recv() + if err != nil { + // Having no messages, we get an EOF so this is not a failure. + if !errors.Is(err, io.EOF) { + t.Fatalf("Got unexpected error during gribi recv request, error: %s", err) + } + } + + // Remote from the perspective of the router. + remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) + localIP, localPort := getHostPortInfo(t, target) + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GRIBI, + RPCPath: "/gribi.gRIBI/Get", + RPCPayload: payload.String(), + LocalIP: localIP, + LocalPort: localPort, + RemoteIP: remoteIP, + RemotePort: remotePort, + Succeeded: true, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, + ExpectedAuthenCause: "authentication_method: local", + ExpectedIdentity: successUsername, + }) + + return records +} + +// SendP4rtRPCs Setup P4RT test RPCs (successful and failed) to be used in the acctz client tests. +func SendP4rtRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { + // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the + // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection + // but that won't get us v4 and v6, it will just get us whatever is configured in binding, + // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. + target := getGrpcTarget(t, dut, introspect.P4RT) + + var records []Record + grpcConn := dialGrpc(t, target) + ctx := context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) + startTime := time.Now() + p4rtclient := p4pb.NewP4RuntimeClient(grpcConn) + _, err := p4rtclient.Capabilities(ctx, &p4pb.CapabilitiesRequest{}) + if err != nil { + t.Logf("Got expected error getting p4rt capabilities with no creds, error: %s", err) + } else { + t.Fatal("Did not get expected error fetching pr4t capabilities with no creds.") + } + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_P4RT, + RPCPath: "/p4.v1.P4Runtime/Capabilities", + RPCPayload: "", + Succeeded: false, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + ExpectedIdentity: failUsername, + }) + + ctx = context.Background() + ctx = metadata.AppendToOutgoingContext(ctx, "username", successUsername) + ctx = metadata.AppendToOutgoingContext(ctx, "password", successPassword) + req := &p4pb.CapabilitiesRequest{} + payload, err := anypb.New(req) + if err != nil { + t.Fatal("Failed creating anypb payload.") + } + startTime = time.Now() + _, err = p4rtclient.Capabilities(ctx, req) + if err != nil { + t.Fatalf("Error fetching p4rt capabilities, error: %s", err) + } + + // Remote from the perspective of the router. + remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) + localIP, localPort := getHostPortInfo(t, target) + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_P4RT, + RPCPath: "/p4.v1.P4Runtime/Capabilities", + RPCPayload: payload.String(), + LocalIP: localIP, + LocalPort: localPort, + RemoteIP: remoteIP, + RemotePort: remotePort, + Succeeded: true, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, + ExpectedAuthenCause: "authentication_method: local", + ExpectedIdentity: successUsername, + }) + + return records +} + +// SendSuccessCliCommand Setup test CLI command (successful) to be used in the acctz client tests. +func SendSuccessCliCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { + // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the + // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we use this workaround + // because ssh isn't exposed in introspection. + target := getSSHTarget(t, dut) + + var records []Record + + sshConn, w := dialSSH(t, successUsername, successPassword, target) + defer func() { + // Give things a second to percolate then close the connection. + time.Sleep(3 * time.Second) + err := sshConn.Close() + if err != nil { + t.Logf("Error closing tcp(ssh) connection, will ignore, error: %s", err) + } + }() + + startTime := time.Now() + + _, err := w.Write([]byte(fmt.Sprintf("%s\n", successCliCommand))) + if err != nil { + t.Fatalf("Failed sending cli command, error: %s", err) + } + + // Remote from the perspective of the router. + remoteIP, remotePort := getHostPortInfo(t, sshConn.LocalAddr().String()) + localIP, localPort := getHostPortInfo(t, target) + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + CmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, + Cmd: successCliCommand, + LocalIP: localIP, + LocalPort: localPort, + RemoteIP: remoteIP, + RemotePort: remotePort, + Succeeded: true, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, + ExpectedAuthenCause: "authentication_method: local", + ExpectedIdentity: successUsername, + }) + + return records +} + +// SendFailCliCommand Setup test CLI command (failed) to be used in the acctz client tests. +func SendFailCliCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { + // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the + // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we use this workaround + // because ssh isn't exposed in introspection. + target := getSSHTarget(t, dut) + + var records []Record + sshConn, w := dialSSH(t, failUsername, failPassword, target) + + defer func() { + // Give things a second to percolate then close the connection. + time.Sleep(3 * time.Second) + err := sshConn.Close() + if err != nil { + t.Logf("Error closing tcp(ssh) connection, will ignore, error: %s", err) + } + }() + + startTime := time.Now() + _, err := w.Write([]byte(fmt.Sprintf("%s\n", failCliCommand))) + if err != nil { + t.Fatalf("Failed sending cli command, error: %s", err) + } + + // Remote from the perspective of the router. + remoteIP, remotePort := getHostPortInfo(t, sshConn.LocalAddr().String()) + localIP, localPort := getHostPortInfo(t, target) + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + CmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, + Cmd: failCliCommand, + LocalIP: localIP, + LocalPort: localPort, + RemoteIP: remoteIP, + RemotePort: remotePort, + Succeeded: true, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, + ExpectedAuthenCause: "authentication_method: local", + ExpectedIdentity: failUsername, + }) + + return records +} + +// SendShellCommand Setup test shell command (successful) to be used in the acctz client tests. +func SendShellCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { + // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the + // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we use this workaround + // because ssh isn't exposed in introspection. + target := getSSHTarget(t, dut) + + var records []Record + shellUsername := successUsername + shellPassword := successPassword + + switch dut.Vendor() { + case ondatra.NOKIA: + // Assuming linuxadmin is present and ssh'ing directly via this user gets us to shell + // straight away so this is easy button to trigger a shell record. + shellUsername = "linuxadmin" + shellPassword = "NokiaSrl1!" + } + + sshConn, w := dialSSH(t, shellUsername, shellPassword, target) + defer func() { + // Give things a second to percolate then close the connection. + time.Sleep(3 * time.Second) + err := sshConn.Close() + if err != nil { + t.Logf("Error closing tcp(ssh) connection, will ignore, error: %s", err) + } + }() + + startTime := time.Now() + + // This might not work for other vendors, so probably we can have a switch here and pass + // the writer to func per vendor if needed. + _, err := w.Write([]byte(fmt.Sprintf("%s\n", shellCommand))) + if err != nil { + t.Fatalf("Failed sending cli command, error: %s", err) + } + + // Remote from the perspective of the router. + remoteIP, remotePort := getHostPortInfo(t, sshConn.LocalAddr().String()) + localIP, localPort := getHostPortInfo(t, target) + + records = append(records, Record{ + StartTime: startTime, + DoneTime: time.Now(), + CmdType: acctz.CommandService_CMD_SERVICE_TYPE_SHELL, + Cmd: shellCommand, + LocalIP: localIP, + LocalPort: localPort, + RemoteIP: remoteIP, + RemotePort: remotePort, + Succeeded: true, + ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, + ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + ExpectedAuthenCause: "", + ExpectedIdentity: shellUsername, + }) + + return records +} From b7b1fd4618bc0c49f5b79918eb977867b5528f51 Mon Sep 17 00:00:00 2001 From: "Dipesh Chauhan (Nokia)" Date: Tue, 1 Oct 2024 16:05:48 -0400 Subject: [PATCH 4/4] Address review comments "This code is a Contribution to the OpenConfig Feature Profiles project ("Work") made under the Google Software Grant and Corporate Contributor License Agreement ("CLA") and governed by the Apache License 2.0. No other rights or licenses in or to any of Nokia's intellectual property are granted for any other purpose. This code is provided on an "as is" basis without any warranties of any kind." --- .../record_subscribe_non_grpc_test.go | 149 ++--- internal/security/acctz/acctz.go | 632 ++++++++++-------- 2 files changed, 406 insertions(+), 375 deletions(-) diff --git a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go index c7677e05411..e41c73b2bcf 100644 --- a/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go +++ b/feature/security/gnsi/acctz/tests/record_subscribe_non_grpc/record_subscribe_non_grpc_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package record_subscribe_non_grpc_test +package recordsubscribenongrpc import ( "context" @@ -20,6 +20,8 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" "github.com/openconfig/featureprofiles/internal/fptest" @@ -44,8 +46,8 @@ func prettyPrint(i interface{}) string { func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { dut := ondatra.DUT(t, "dut") - acctz.SetupSSHUsers(t, dut) - var records []acctz.Record + acctz.SetupUsers(t, dut, true) + var records []*acctzpb.RecordResponse // Put enough time between the test starting and any prior events so we can easily know where // our records start. @@ -63,28 +65,29 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { time.Sleep(5 * time.Second) // Get gNSI record subscribe client. - acctzClient := dut.RawAPIs().GNSI(t).Acctz() - - acctzSubClient, err := acctzClient.RecordSubscribe(context.Background()) - if err != nil { - t.Fatalf("Failed getting accountz record subscribe client, error: %s", err) + requestTimestamp := ×tamppb.Timestamp{ + Seconds: 0, + Nanos: 0, } - - // This will have to move up to RecordSubscribe call after this is brought into FP/Ondatra. - // https://github.com/openconfig/gnsi/pull/149/files - err = acctzSubClient.Send(&acctzpb.RecordRequest{ - Timestamp: ×tamppb.Timestamp{ - Seconds: 0, - Nanos: 0, - }, - }) + acctzClient := dut.RawAPIs().GNSI(t).AcctzStream() + acctzSubClient, err := acctzClient.RecordSubscribe(context.Background(), &acctzpb.RecordRequest{Timestamp: requestTimestamp}) if err != nil { t.Fatalf("Failed sending accountz record request, error: %s", err) } + defer acctzSubClient.CloseSend() var recordIdx int var lastTimestampUnixMillis int64 var lastTaskID string + r := make(chan recordRequestResult) + + // Ignore proto fields which are set internally by the DUT (cannot be matched exactly) + // and compare them manually later. + popts := []cmp.Option{protocmp.Transform(), + protocmp.IgnoreFields(&acctzpb.RecordResponse{}, "timestamp", "task_ids"), + protocmp.IgnoreFields(&acctzpb.AuthzDetail{}, "detail"), + protocmp.IgnoreFields(&acctzpb.SessionInfo{}, "channel_id", "tty"), + } for { if recordIdx >= len(records) { @@ -92,8 +95,7 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { break } - r := make(chan recordRequestResult) - + // Read single acctz record from stream into channel. go func(r chan recordRequestResult) { var response *acctzpb.RecordResponse response, err = acctzSubClient.Recv() @@ -104,9 +106,10 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { }(r) var done bool - var resp recordRequestResult + // Read acctz record from channel for evaluation. + // Timeout and exit if no records received on the channel for some time. select { case rr := <-r: resp = rr @@ -123,20 +126,11 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { t.Fatalf("Failed receiving record response, error: %s", resp.err) } - if resp.record.GetHistoryIstruncated() { - t.Fatalf("History is truncated but it shouldn't be, Record Details: %s", prettyPrint(resp.record)) - } - if !resp.record.Timestamp.AsTime().After(startTime) { // Skipping record if it happened before test start time. continue } - if resp.record.GetGrpcService().GetServiceType() != acctzpb.GrpcService_GRPC_SERVICE_TYPE_UNSPECIFIED { - // Skipping gRPC records (if any). - continue - } - // Some task ids may be tracked multiple times (for start/stop accounting). If we see two in // a row that are the same task, we can skip this record and continue. currentTaskID := resp.record.TaskIds[0] @@ -145,42 +139,21 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { } lastTaskID = currentTaskID - // Check that the timestamp for the record is between our start/stop times for our cmd. timestamp := resp.record.Timestamp.AsTime() if timestamp.UnixMilli() == lastTimestampUnixMillis { // This ensures that timestamps are actually changing for each record. - t.Fatalf("Timestamp is the same as the previous timestamp, this shouldn't be possible!, Record Details: %s", prettyPrint(resp.record)) + t.Errorf("Timestamp is the same as the previous timestamp, this shouldn't be possible!, Record Details: %s", prettyPrint(resp.record)) } lastTimestampUnixMillis = timestamp.UnixMilli() - // -2 for a little breathing room since things may not be perfectly synced up time-wise. - if records[recordIdx].StartTime.Unix()-2 > timestamp.Unix() { - t.Fatalf( - "Record timestamp is prior to cmd start time timestamp, cmd start timestamp %d, record timestamp %d, Record Details: %s", - records[recordIdx].StartTime.Unix()-2, - timestamp.Unix(), - prettyPrint(resp.record), - ) - } - - // Done time (that we recorded when making the cmd) + 2 second for some breathing room. - if records[recordIdx].DoneTime.Unix()+2 < timestamp.Unix() { - t.Fatalf( - "Record timestamp is after cmd end timestamp, cmd end timestamp %d, record timestamp %d, Record Details: %s", - records[recordIdx].DoneTime.Unix()+2, - timestamp.Unix(), - prettyPrint(resp.record), - ) + // Verify acctz proto bits. + if diff := cmp.Diff(resp.record, records[recordIdx], popts...); diff != "" { + t.Errorf("got diff in got/want: %s", diff) } - cmdType := resp.record.GetCmdService().GetServiceType() - if records[recordIdx].CmdType != cmdType { - t.Fatalf("Service type not correct, got %q, want %q, Record Details: %s", cmdType, records[recordIdx].CmdType, prettyPrint(resp.record)) - } - - cmd := resp.record.GetCmdService().GetCmd() - if records[recordIdx].Cmd != cmd { - t.Fatalf("Command not correct, got %q, want %q, Record Details: %s", cmd, records[recordIdx].Cmd, prettyPrint(resp.record)) + // Verify record timestamp is after request timestamp. + if !timestamp.After(requestTimestamp.AsTime()) { + t.Errorf("Record timestamp is before record request timestamp %v, Record Details: %v", requestTimestamp.AsTime(), prettyPrint(resp.record)) } // This channel check maybe should just go away entirely -- see: @@ -188,65 +161,19 @@ func TestAccountzRecordSubscribeNonGRPC(t *testing.T) { // In case of Nokia this is being set to the aaa session id just to have some hopefully // useful info in this field to identify a "session" (even if it isn't necessarily ssh/grpc // directly). - channelID := resp.record.GetSessionInfo().GetChannelId() - if !records[recordIdx].Succeeded { - if channelID != "aaa_session_id: 0" { - t.Fatalf("Auth was not successful for this record, but channel id was set, got %q, Record Details: %s", channelID, prettyPrint(resp.record)) - } + if resp.record.GetSessionInfo().GetChannelId() == "" { + t.Errorf("Channel Id is not populated for record: %v", prettyPrint(resp.record)) } // Tty only set for ssh records. - tty := resp.record.GetSessionInfo().GetTty() - if tty == "" { - t.Fatalf("Should have tty allocated but not set, Record Details: %s", prettyPrint(resp.record)) + if resp.record.GetSessionInfo().GetTty() == "" { + t.Errorf("Should have tty allocated but not set, Record Details: %s", prettyPrint(resp.record)) } - sessionStatus := resp.record.GetSessionInfo().GetStatus() - if records[recordIdx].ExpectedStatus != sessionStatus { - t.Fatalf("Session status not correct, got %q, want %q, Record Details: %s", sessionStatus, records[recordIdx].ExpectedStatus, prettyPrint(resp.record)) - } - - authenType := resp.record.GetSessionInfo().GetAuthn().GetType() - if records[recordIdx].ExpectedAuthenType != authenType { - t.Fatalf("AuthenType not correct, got %q, want %q, Record Details: %s", authenType, records[recordIdx].ExpectedAuthenType, prettyPrint(resp.record)) - } - - authenStatus := resp.record.GetSessionInfo().GetAuthn().GetStatus() - if records[recordIdx].ExpectedAuthenStatus != authenStatus { - t.Fatalf("AuthenStatus not correct, got %q, want %q, Record Details: %s", authenStatus, records[recordIdx].ExpectedAuthenStatus, prettyPrint(resp.record)) - } - - authenCause := resp.record.GetSessionInfo().GetAuthn().GetCause() - if records[recordIdx].ExpectedAuthenCause != authenCause { - t.Fatalf("AuthenCause not correct, got %q, want %q, Record Details: %s", authenCause, records[recordIdx].ExpectedAuthenCause, prettyPrint(resp.record)) - } - - userIdentity := resp.record.GetSessionInfo().GetUser().GetIdentity() - if records[recordIdx].ExpectedIdentity != userIdentity { - t.Fatalf("Identity not correct, got %q, want %q, Record Details: %s", userIdentity, records[recordIdx].ExpectedIdentity, prettyPrint(resp.record)) - } - - if records[recordIdx].Succeeded { - // Verify the l4 bits align, this is only set if auth is successful so do it down here. - localAddr := resp.record.GetSessionInfo().GetLocalAddress() - if records[recordIdx].LocalIP != localAddr { - t.Fatalf("Local address not correct, got %q, want %q, Record Details: %s", localAddr, records[recordIdx].LocalIP, prettyPrint(resp.record)) - } - - localPort := resp.record.GetSessionInfo().GetLocalPort() - if records[recordIdx].LocalPort != localPort { - t.Fatalf("Local port not correct, got %d, want %d, Record Details: %s", localPort, records[recordIdx].LocalPort, prettyPrint(resp.record)) - } - - remoteAddr := resp.record.GetSessionInfo().GetRemoteAddress() - if records[recordIdx].RemoteIP != remoteAddr { - t.Fatalf("Remote address not correct, got %q, want %q, Record Details: %s", remoteAddr, records[recordIdx].RemoteIP, prettyPrint(resp.record)) - } - - remotePort := resp.record.GetSessionInfo().GetRemotePort() - if records[recordIdx].RemotePort != remotePort { - t.Fatalf("Remote port not correct, got %d, want %d, Record Details: %s", remotePort, records[recordIdx].RemotePort, prettyPrint(resp.record)) - } + // Verify authz detail is populated for denied cmds. + authzInfo := resp.record.GetCmdService().GetAuthz() + if authzInfo.Status == acctzpb.AuthzDetail_AUTHZ_STATUS_DENY && authzInfo.GetDetail() == "" { + t.Errorf("Authorization detail is not populated for record: %v", prettyPrint(resp.record)) } t.Logf("Processed Record: %s", prettyPrint(resp.record)) diff --git a/internal/security/acctz/acctz.go b/internal/security/acctz/acctz.go index b45cc4a270c..6837c82f0bc 100644 --- a/internal/security/acctz/acctz.go +++ b/internal/security/acctz/acctz.go @@ -29,9 +29,9 @@ import ( "github.com/openconfig/gnmi/proto/gnmi" "github.com/openconfig/gnoi/system" - "github.com/openconfig/gnsi/acctz" + acctzpb "github.com/openconfig/gnsi/acctz" authzpb "github.com/openconfig/gnsi/authz" - "github.com/openconfig/gnsi/credentialz" + cpb "github.com/openconfig/gnsi/credentialz" gribi "github.com/openconfig/gribi/v1/proto/service" tpb "github.com/openconfig/kne/proto/topo" "github.com/openconfig/ondatra" @@ -59,41 +59,23 @@ const ( gnmiCapabilitiesPath = "/gnmi.gNMI/Capabilities" gnoiPingPath = "/gnoi.system.System/Ping" gnsiGetPath = "/gnsi.authz.v1.Authz/Get" + gribiGetPath = "/gribi.gRIBI/Get" + p4rtCapabilitiesPath = "/p4.v1.P4Runtime/Capabilities" defaultSSHPort = 22 + ipProto = 6 ) var gRPCClientAddr net.Addr -// Record represents the structure for an acctz record. -type Record struct { - StartTime time.Time - DoneTime time.Time - CmdType acctz.CommandService_CmdServiceType - Cmd string - RPCType acctz.GrpcService_GrpcServiceType - RPCPath string - RPCPayload string - LocalIP string - LocalPort uint32 - RemoteIP string - RemotePort uint32 - Succeeded bool - ExpectedStatus acctz.SessionInfo_SessionStatus - ExpectedAuthenType acctz.AuthnDetail_AuthnType - ExpectedAuthenStatus acctz.AuthnDetail_AuthnStatus - ExpectedAuthenCause string - ExpectedIdentity string -} - func setupUserPassword(t *testing.T, dut *ondatra.DUTDevice, username, password string) { - request := &credentialz.RotateAccountCredentialsRequest{ - Request: &credentialz.RotateAccountCredentialsRequest_Password{ - Password: &credentialz.PasswordRequest{ - Accounts: []*credentialz.PasswordRequest_Account{ + request := &cpb.RotateAccountCredentialsRequest{ + Request: &cpb.RotateAccountCredentialsRequest_Password{ + Password: &cpb.PasswordRequest{ + Accounts: []*cpb.PasswordRequest_Account{ { Account: username, - Password: &credentialz.PasswordRequest_Password{ - Value: &credentialz.PasswordRequest_Password_Plaintext{ + Password: &cpb.PasswordRequest_Password{ + Value: &cpb.PasswordRequest_Password_Plaintext{ Plaintext: password, }, }, @@ -118,8 +100,8 @@ func setupUserPassword(t *testing.T, dut *ondatra.DUTDevice, username, password if err != nil { t.Fatalf("Failed receiving credentialz rotate account credentials response, error: %s", err) } - err = credzRotateClient.Send(&credentialz.RotateAccountCredentialsRequest{ - Request: &credentialz.RotateAccountCredentialsRequest_Finalize{ + err = credzRotateClient.Send(&cpb.RotateAccountCredentialsRequest{ + Request: &cpb.RotateAccountCredentialsRequest_Finalize{ Finalize: request.GetFinalize(), }, }) @@ -168,37 +150,28 @@ func nokiaFailCliRole(t *testing.T) *gnmi.SetRequest { } } -// SetupGrpcUsers Setup users for grpc-based acctz tests. -func SetupGrpcUsers(t *testing.T, dut *ondatra.DUTDevice) { +// SetupUsers Setup users for acctz tests and optionally configure cli role for denied commands. +func SetupUsers(t *testing.T, dut *ondatra.DUTDevice, configureFailCliRole bool) { auth := &oc.System_Aaa_Authentication{} successUser := auth.GetOrCreateUser(successUsername) successUser.SetRole(oc.AaaTypes_SYSTEM_DEFINED_ROLES_SYSTEM_ROLE_ADMIN) - auth.GetOrCreateUser(failUsername) - ondatragnmi.Update(t, dut, ondatragnmi.OC().System().Aaa().Authentication().Config(), auth) - setupUserPassword(t, dut, successUsername, successPassword) -} + failUser := auth.GetOrCreateUser(failUsername) + if configureFailCliRole { + var SetRequest *gnmi.SetRequest -// SetupSSHUsers Setup users for ssh-based acctz tests. -func SetupSSHUsers(t *testing.T, dut *ondatra.DUTDevice) { - var SetRequest *gnmi.SetRequest + // Create failure cli role in native. + switch dut.Vendor() { + case ondatra.NOKIA: + SetRequest = nokiaFailCliRole(t) + } - // Create failure cli role in native. - switch dut.Vendor() { - case ondatra.NOKIA: - SetRequest = nokiaFailCliRole(t) - } + gnmiClient := dut.RawAPIs().GNMI(t) + if _, err := gnmiClient.Set(context.Background(), SetRequest); err != nil { + t.Fatalf("Unexpected error configuring role: %v", err) + } - gnmiClient := dut.RawAPIs().GNMI(t) - if _, err := gnmiClient.Set(context.Background(), SetRequest); err != nil { - t.Fatalf("Unexpected error configuring role: %v", err) + failUser.SetRole(oc.UnionString(failRoleName)) } - - // Configure users. - auth := &oc.System_Aaa_Authentication{} - successUser := auth.GetOrCreateUser(successUsername) - successUser.SetRole(oc.AaaTypes_SYSTEM_DEFINED_ROLES_SYSTEM_ROLE_ADMIN) - failUser := auth.GetOrCreateUser(failUsername) - failUser.SetRole(oc.UnionString(failRoleName)) ondatragnmi.Update(t, dut, ondatragnmi.OC().System().Aaa().Authentication().Config(), auth) setupUserPassword(t, dut, successUsername, successPassword) setupUserPassword(t, dut, failUsername, failPassword) @@ -236,7 +209,7 @@ func getSSHTarget(t *testing.T, dut *ondatra.DUTDevice) string { if err != nil { t.Fatal(err) } - target = fmt.Sprintf("%s:%d", dutSSHService.GetOutsideIp(), defaultSSHPort) + target = fmt.Sprintf("%s:%d", dutSSHService.GetOutsideIp(), dutSSHService.GetOutside()) } t.Logf("Target for ssh service: %s", target) @@ -343,20 +316,19 @@ func getHostPortInfo(t *testing.T, address string) (string, uint32) { } // SendGnmiRPCs Setup gNMI test RPCs (successful and failed) to be used in the acctz client tests. -func SendGnmiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { +func SendGnmiRPCs(t *testing.T, dut *ondatra.DUTDevice) []*acctzpb.RecordResponse { // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection // but that won't get us v4 and v6, it will just get us whatever is configured in binding, // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. target := getGrpcTarget(t, dut, introspect.GNMI) - var records []Record + var records []*acctzpb.RecordResponse grpcConn := dialGrpc(t, target) gnmiClient := gnmi.NewGNMIClient(grpcConn) ctx := context.Background() ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) - startTime := time.Now() // Send an unsuccessful gNMI capabilities request (bad creds in context). _, err := gnmiClient.Capabilities(ctx, &gnmi.CapabilityRequest{}) @@ -366,16 +338,26 @@ func SendGnmiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { t.Fatal("Did not get expected error fetching capabilities with bad creds.") } - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNMI, - RPCPath: gnmiCapabilitiesPath, - Succeeded: false, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, - ExpectedIdentity: failUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_GNMI, + RpcName: gnmiCapabilitiesPath, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_DENY, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + }, + User: &acctzpb.UserDetail{ + Identity: failUsername, + }, + }, }) // Send a successful gNMI capabilities request. @@ -387,7 +369,6 @@ func SendGnmiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { if err != nil { t.Fatal("Failed creating anypb payload.") } - startTime = time.Now() _, err = gnmiClient.Capabilities(ctx, req) if err != nil { t.Fatalf("Error fetching capabilities, error: %s", err) @@ -397,42 +378,54 @@ func SendGnmiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) localIP, localPort := getHostPortInfo(t, target) - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNMI, - RPCPath: gnmiCapabilitiesPath, - RPCPayload: payload.String(), - LocalIP: localIP, - LocalPort: localPort, - RemoteIP: remoteIP, - RemotePort: remotePort, - Succeeded: true, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, - ExpectedAuthenCause: "authentication_method: local", - ExpectedIdentity: successUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_GNMI, + RpcName: gnmiCapabilitiesPath, + Payload: &acctzpb.GrpcService_ProtoVal{ + ProtoVal: payload, + }, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_PERMIT, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + LocalAddress: localIP, + LocalPort: localPort, + RemoteAddress: remoteIP, + RemotePort: remotePort, + IpProto: ipProto, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_SUCCESS, + Cause: "authentication_method: local", + }, + User: &acctzpb.UserDetail{ + Identity: successUsername, + }, + }, }) return records } // SendGnoiRPCs Setup gNOI test RPCs (successful and failed) to be used in the acctz client tests. -func SendGnoiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { +func SendGnoiRPCs(t *testing.T, dut *ondatra.DUTDevice) []*acctzpb.RecordResponse { // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection // but that won't get us v4 and v6, it will just get us whatever is configured in binding, // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. target := getGrpcTarget(t, dut, introspect.GNOI) - var records []Record + var records []*acctzpb.RecordResponse grpcConn := dialGrpc(t, target) gnoiSystemClient := system.NewSystemClient(grpcConn) ctx := context.Background() ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) - startTime := time.Now() // Send an unsuccessful gNOI system time request (bad creds in context), we don't // care about receiving on it, just want to make the request. @@ -449,16 +442,26 @@ func SendGnoiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { t.Logf("Got expected error getting gnoi system time with bad creds, error: %s", err) } - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNOI, - RPCPath: gnoiPingPath, - Succeeded: false, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, - ExpectedIdentity: failUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_GNOI, + RpcName: gnoiPingPath, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_DENY, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + }, + User: &acctzpb.UserDetail{ + Identity: failUsername, + }, + }, }) // Send a successful gNOI ping request. @@ -473,7 +476,6 @@ func SendGnoiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { if err != nil { t.Fatal("Failed creating anypb payload.") } - startTime = time.Now() gnoiSystemPingClient, err = gnoiSystemClient.Ping(ctx, req) if err != nil { t.Fatalf("Error fetching gnoi system time, error: %s", err) @@ -487,42 +489,54 @@ func SendGnoiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) localIP, localPort := getHostPortInfo(t, target) - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNOI, - RPCPath: gnoiPingPath, - RPCPayload: payload.String(), - LocalIP: localIP, - LocalPort: localPort, - RemoteIP: remoteIP, - RemotePort: remotePort, - Succeeded: true, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, - ExpectedAuthenCause: "authentication_method: local", - ExpectedIdentity: successUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_GNOI, + RpcName: gnoiPingPath, + Payload: &acctzpb.GrpcService_ProtoVal{ + ProtoVal: payload, + }, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_PERMIT, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + LocalAddress: localIP, + LocalPort: localPort, + RemoteAddress: remoteIP, + RemotePort: remotePort, + IpProto: ipProto, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_SUCCESS, + Cause: "authentication_method: local", + }, + User: &acctzpb.UserDetail{ + Identity: successUsername, + }, + }, }) return records } // SendGnsiRPCs Setup gNSI test RPCs (successful and failed) to be used in the acctz client tests. -func SendGnsiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { +func SendGnsiRPCs(t *testing.T, dut *ondatra.DUTDevice) []*acctzpb.RecordResponse { // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection // but that won't get us v4 and v6, it will just get us whatever is configured in binding, // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. target := getGrpcTarget(t, dut, introspect.GNSI) - var records []Record + var records []*acctzpb.RecordResponse grpcConn := dialGrpc(t, target) authzClient := authzpb.NewAuthzClient(grpcConn) ctx := context.Background() ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) - startTime := time.Now() // Send an unsuccessful gNSI authz get request (bad creds in context), we don't // care about receiving on it, just want to make the request. @@ -533,16 +547,26 @@ func SendGnsiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { t.Fatal("Did not get expected error fetching authz policy with bad creds.") } - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNSI, - RPCPath: gnsiGetPath, - Succeeded: false, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, - ExpectedIdentity: failUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_GNSI, + RpcName: gnsiGetPath, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_DENY, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + }, + User: &acctzpb.UserDetail{ + Identity: failUsername, + }, + }, }) // Send a successful gNSI authz get request. @@ -554,7 +578,6 @@ func SendGnsiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { if err != nil { t.Fatal("Failed creating anypb payload.") } - startTime = time.Now() _, err = authzClient.Get(ctx, &authzpb.GetRequest{}) if err != nil { t.Fatalf("Error fetching authz policy, error: %s", err) @@ -564,42 +587,54 @@ func SendGnsiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) localIP, localPort := getHostPortInfo(t, target) - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GNSI, - RPCPath: gnsiGetPath, - RPCPayload: payload.String(), - LocalIP: localIP, - LocalPort: localPort, - RemoteIP: remoteIP, - RemotePort: remotePort, - Succeeded: true, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, - ExpectedAuthenCause: "authentication_method: local", - ExpectedIdentity: successUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_GNSI, + RpcName: gnsiGetPath, + Payload: &acctzpb.GrpcService_ProtoVal{ + ProtoVal: payload, + }, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_PERMIT, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + LocalAddress: localIP, + LocalPort: localPort, + RemoteAddress: remoteIP, + RemotePort: remotePort, + IpProto: ipProto, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_SUCCESS, + Cause: "authentication_method: local", + }, + User: &acctzpb.UserDetail{ + Identity: successUsername, + }, + }, }) return records } // SendGribiRPCs Setup gRIBI test RPCs (successful and failed) to be used in the acctz client tests. -func SendGribiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { +func SendGribiRPCs(t *testing.T, dut *ondatra.DUTDevice) []*acctzpb.RecordResponse { // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection // but that won't get us v4 and v6, it will just get us whatever is configured in binding, // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. target := getGrpcTarget(t, dut, introspect.GRIBI) - var records []Record + var records []*acctzpb.RecordResponse grpcConn := dialGrpc(t, target) gribiClient := gribi.NewGRIBIClient(grpcConn) ctx := context.Background() ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) - startTime := time.Now() // Send an unsuccessful gRIBI get request (bad creds in context), we don't // care about receiving on it, just want to make the request. @@ -618,17 +653,26 @@ func SendGribiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { t.Logf("Got expected error during gribi recv request, error: %s", err) } - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GRIBI, - RPCPath: "/gribi.gRIBI/Get", - RPCPayload: "", - Succeeded: false, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, - ExpectedIdentity: failUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_GRIBI, + RpcName: gribiGetPath, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_DENY, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + }, + User: &acctzpb.UserDetail{ + Identity: failUsername, + }, + }, }) // Send a successful gRIBI get request. @@ -643,7 +687,6 @@ func SendGribiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { if err != nil { t.Fatal("Failed creating anypb payload.") } - startTime = time.Now() gribiGetClient, err = gribiClient.Get(ctx, req) if err != nil { t.Fatalf("Got unexpected error during gribi get request, error: %s", err) @@ -660,41 +703,53 @@ func SendGribiRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) localIP, localPort := getHostPortInfo(t, target) - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_GRIBI, - RPCPath: "/gribi.gRIBI/Get", - RPCPayload: payload.String(), - LocalIP: localIP, - LocalPort: localPort, - RemoteIP: remoteIP, - RemotePort: remotePort, - Succeeded: true, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, - ExpectedAuthenCause: "authentication_method: local", - ExpectedIdentity: successUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_GRIBI, + RpcName: gribiGetPath, + Payload: &acctzpb.GrpcService_ProtoVal{ + ProtoVal: payload, + }, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_PERMIT, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + LocalAddress: localIP, + LocalPort: localPort, + RemoteAddress: remoteIP, + RemotePort: remotePort, + IpProto: ipProto, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_SUCCESS, + Cause: "authentication_method: local", + }, + User: &acctzpb.UserDetail{ + Identity: successUsername, + }, + }, }) return records } // SendP4rtRPCs Setup P4RT test RPCs (successful and failed) to be used in the acctz client tests. -func SendP4rtRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { +func SendP4rtRPCs(t *testing.T, dut *ondatra.DUTDevice) []*acctzpb.RecordResponse { // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we just use introspection // but that won't get us v4 and v6, it will just get us whatever is configured in binding, // so while the test asks for v4 and v6 we'll just be doing it for whatever we get. target := getGrpcTarget(t, dut, introspect.P4RT) - var records []Record + var records []*acctzpb.RecordResponse grpcConn := dialGrpc(t, target) ctx := context.Background() ctx = metadata.AppendToOutgoingContext(ctx, "username", failUsername) ctx = metadata.AppendToOutgoingContext(ctx, "password", failPassword) - startTime := time.Now() p4rtclient := p4pb.NewP4RuntimeClient(grpcConn) _, err := p4rtclient.Capabilities(ctx, &p4pb.CapabilitiesRequest{}) if err != nil { @@ -703,17 +758,26 @@ func SendP4rtRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { t.Fatal("Did not get expected error fetching pr4t capabilities with no creds.") } - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_P4RT, - RPCPath: "/p4.v1.P4Runtime/Capabilities", - RPCPayload: "", - Succeeded: false, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, - ExpectedIdentity: failUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_P4RT, + RpcName: p4rtCapabilitiesPath, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_DENY, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + }, + User: &acctzpb.UserDetail{ + Identity: failUsername, + }, + }, }) ctx = context.Background() @@ -724,7 +788,6 @@ func SendP4rtRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { if err != nil { t.Fatal("Failed creating anypb payload.") } - startTime = time.Now() _, err = p4rtclient.Capabilities(ctx, req) if err != nil { t.Fatalf("Error fetching p4rt capabilities, error: %s", err) @@ -734,35 +797,48 @@ func SendP4rtRPCs(t *testing.T, dut *ondatra.DUTDevice) []Record { remoteIP, remotePort := getHostPortInfo(t, gRPCClientAddr.String()) localIP, localPort := getHostPortInfo(t, target) - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - RPCType: acctz.GrpcService_GRPC_SERVICE_TYPE_P4RT, - RPCPath: "/p4.v1.P4Runtime/Capabilities", - RPCPayload: payload.String(), - LocalIP: localIP, - LocalPort: localPort, - RemoteIP: remoteIP, - RemotePort: remotePort, - Succeeded: true, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_ONCE, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, - ExpectedAuthenCause: "authentication_method: local", - ExpectedIdentity: successUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_GrpcService{ + GrpcService: &acctzpb.GrpcService{ + ServiceType: acctzpb.GrpcService_GRPC_SERVICE_TYPE_P4RT, + RpcName: p4rtCapabilitiesPath, + Payload: &acctzpb.GrpcService_ProtoVal{ + ProtoVal: payload, + }, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_PERMIT, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_ONCE, + LocalAddress: localIP, + LocalPort: localPort, + RemoteAddress: remoteIP, + RemotePort: remotePort, + IpProto: ipProto, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_SUCCESS, + Cause: "authentication_method: local", + }, + User: &acctzpb.UserDetail{ + Identity: successUsername, + }, + }, }) return records } // SendSuccessCliCommand Setup test CLI command (successful) to be used in the acctz client tests. -func SendSuccessCliCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { +func SendSuccessCliCommand(t *testing.T, dut *ondatra.DUTDevice) []*acctzpb.RecordResponse { // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we use this workaround // because ssh isn't exposed in introspection. target := getSSHTarget(t, dut) - var records []Record + var records []*acctzpb.RecordResponse sshConn, w := dialSSH(t, successUsername, successPassword, target) defer func() { @@ -774,8 +850,6 @@ func SendSuccessCliCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { } }() - startTime := time.Now() - _, err := w.Write([]byte(fmt.Sprintf("%s\n", successCliCommand))) if err != nil { t.Fatalf("Failed sending cli command, error: %s", err) @@ -785,34 +859,45 @@ func SendSuccessCliCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { remoteIP, remotePort := getHostPortInfo(t, sshConn.LocalAddr().String()) localIP, localPort := getHostPortInfo(t, target) - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - CmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, - Cmd: successCliCommand, - LocalIP: localIP, - LocalPort: localPort, - RemoteIP: remoteIP, - RemotePort: remotePort, - Succeeded: true, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, - ExpectedAuthenCause: "authentication_method: local", - ExpectedIdentity: successUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_CmdService{ + CmdService: &acctzpb.CommandService{ + ServiceType: acctzpb.CommandService_CMD_SERVICE_TYPE_CLI, + Cmd: successCliCommand, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_PERMIT, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_OPERATION, + LocalAddress: localIP, + LocalPort: localPort, + RemoteAddress: remoteIP, + RemotePort: remotePort, + IpProto: ipProto, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_SUCCESS, + Cause: "authentication_method: local", + }, + User: &acctzpb.UserDetail{ + Identity: successUsername, + }, + }, }) return records } // SendFailCliCommand Setup test CLI command (failed) to be used in the acctz client tests. -func SendFailCliCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { +func SendFailCliCommand(t *testing.T, dut *ondatra.DUTDevice) []*acctzpb.RecordResponse { // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we use this workaround // because ssh isn't exposed in introspection. target := getSSHTarget(t, dut) - var records []Record + var records []*acctzpb.RecordResponse sshConn, w := dialSSH(t, failUsername, failPassword, target) defer func() { @@ -824,7 +909,6 @@ func SendFailCliCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { } }() - startTime := time.Now() _, err := w.Write([]byte(fmt.Sprintf("%s\n", failCliCommand))) if err != nil { t.Fatalf("Failed sending cli command, error: %s", err) @@ -834,34 +918,46 @@ func SendFailCliCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { remoteIP, remotePort := getHostPortInfo(t, sshConn.LocalAddr().String()) localIP, localPort := getHostPortInfo(t, target) - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - CmdType: acctz.CommandService_CMD_SERVICE_TYPE_CLI, - Cmd: failCliCommand, - LocalIP: localIP, - LocalPort: localPort, - RemoteIP: remoteIP, - RemotePort: remotePort, - Succeeded: true, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_SUCCESS, - ExpectedAuthenCause: "authentication_method: local", - ExpectedIdentity: failUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_CmdService{ + CmdService: &acctzpb.CommandService{ + ServiceType: acctzpb.CommandService_CMD_SERVICE_TYPE_CLI, + Cmd: failCliCommand, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_DENY, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_OPERATION, + LocalAddress: localIP, + LocalPort: localPort, + RemoteAddress: remoteIP, + RemotePort: remotePort, + IpProto: ipProto, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_SUCCESS, + Cause: "authentication_method: local", + }, + User: &acctzpb.UserDetail{ + Identity: failUsername, + Role: failRoleName, + }, + }, }) return records } // SendShellCommand Setup test shell command (successful) to be used in the acctz client tests. -func SendShellCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { +func SendShellCommand(t *testing.T, dut *ondatra.DUTDevice) []*acctzpb.RecordResponse { // Per https://github.com/openconfig/featureprofiles/issues/2637, waiting to see what the // "best"/"preferred" way is to get the v4/v6 of the dut. For now, we use this workaround // because ssh isn't exposed in introspection. target := getSSHTarget(t, dut) - var records []Record + var records []*acctzpb.RecordResponse shellUsername := successUsername shellPassword := successPassword @@ -883,8 +979,6 @@ func SendShellCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { } }() - startTime := time.Now() - // This might not work for other vendors, so probably we can have a switch here and pass // the writer to func per vendor if needed. _, err := w.Write([]byte(fmt.Sprintf("%s\n", shellCommand))) @@ -896,21 +990,31 @@ func SendShellCommand(t *testing.T, dut *ondatra.DUTDevice) []Record { remoteIP, remotePort := getHostPortInfo(t, sshConn.LocalAddr().String()) localIP, localPort := getHostPortInfo(t, target) - records = append(records, Record{ - StartTime: startTime, - DoneTime: time.Now(), - CmdType: acctz.CommandService_CMD_SERVICE_TYPE_SHELL, - Cmd: shellCommand, - LocalIP: localIP, - LocalPort: localPort, - RemoteIP: remoteIP, - RemotePort: remotePort, - Succeeded: true, - ExpectedStatus: acctz.SessionInfo_SESSION_STATUS_OPERATION, - ExpectedAuthenType: acctz.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, - ExpectedAuthenStatus: acctz.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, - ExpectedAuthenCause: "", - ExpectedIdentity: shellUsername, + records = append(records, &acctzpb.RecordResponse{ + ServiceRequest: &acctzpb.RecordResponse_CmdService{ + CmdService: &acctzpb.CommandService{ + ServiceType: acctzpb.CommandService_CMD_SERVICE_TYPE_SHELL, + Cmd: shellCommand, + Authz: &acctzpb.AuthzDetail{ + Status: acctzpb.AuthzDetail_AUTHZ_STATUS_PERMIT, + }, + }, + }, + SessionInfo: &acctzpb.SessionInfo{ + Status: acctzpb.SessionInfo_SESSION_STATUS_OPERATION, + LocalAddress: localIP, + LocalPort: localPort, + RemoteAddress: remoteIP, + RemotePort: remotePort, + IpProto: ipProto, + Authn: &acctzpb.AuthnDetail{ + Type: acctzpb.AuthnDetail_AUTHN_TYPE_UNSPECIFIED, + Status: acctzpb.AuthnDetail_AUTHN_STATUS_UNSPECIFIED, + }, + User: &acctzpb.UserDetail{ + Identity: shellUsername, + }, + }, }) return records