From 7358b87ad83953ffa80a9cfce3e16cfaff13c4b3 Mon Sep 17 00:00:00 2001 From: kanchanavelusamy Date: Mon, 8 Sep 2025 12:47:18 +0000 Subject: [PATCH] Implements the frontend logic for gNSI Authz Signed-off-by: kanchanavelusamy --- gnmi_server/gnsi_authz.go | 292 +++++++++++ gnmi_server/gnsi_authz_test.go | 924 +++++++++++++++++++++++++++++++++ gnmi_server/gnsi_util.go | 60 +++ gnmi_server/gnsi_util_test.go | 254 +++++++++ gnmi_server/server.go | 69 ++- gnmi_server/server_test.go | 10 +- go.mod | 8 +- go.sum | 11 + telemetry/telemetry.go | 9 + testdata/gnsi/authz_meta.json | 1 + 10 files changed, 1618 insertions(+), 20 deletions(-) create mode 100644 gnmi_server/gnsi_authz.go create mode 100644 gnmi_server/gnsi_authz_test.go create mode 100644 gnmi_server/gnsi_util.go create mode 100644 gnmi_server/gnsi_util_test.go create mode 100644 testdata/gnsi/authz_meta.json diff --git a/gnmi_server/gnsi_authz.go b/gnmi_server/gnsi_authz.go new file mode 100644 index 000000000..8b66a9761 --- /dev/null +++ b/gnmi_server/gnsi_authz.go @@ -0,0 +1,292 @@ +package gnmi + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + log "github.com/golang/glog" + "github.com/openconfig/gnsi/authz" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "io" + "os" + "path/filepath" + "strconv" + "sync" + "time" +) + +var ( + authzMu sync.Mutex +) + +var ( + // Step 1: Point the variable to the real function by default + authenticateFunc = authenticate +) + +const ( + authzP4rtTbl string = "AUTHZ_POLICY|p4rt" + authzGnxiTbl string = "AUTHZ_POLICY|gnxi" + authzVersionFld string = "authz_version" + authzCreatedOnFld string = "authz_created_on" +) + +type GNSIAuthzServer struct { + *Server + authzMetadata *AuthzMetadata + authzMetadataCopy AuthzMetadata + authz.UnimplementedAuthzServer +} + +func (srv *GNSIAuthzServer) Probe(context.Context, *authz.ProbeRequest) (*authz.ProbeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Probe not implemented") +} +func (srv *GNSIAuthzServer) Get(context.Context, *authz.GetRequest) (*authz.GetResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") +} +func NewGNSIAuthzServer(srv *Server) *GNSIAuthzServer { + ret := &GNSIAuthzServer{ + Server: srv, + authzMetadata: NewAuthzMetadata(), + } + log.V(2).Infof("gnsi: loading authz metadata from %s", srv.config.AuthzMetaFile) + log.V(2).Infof("gnsi: loading authz policy from %s", srv.config.AuthzPolicyFile) + if err := ret.loadAuthzFreshness(srv.config.AuthzMetaFile); err != nil { + log.V(0).Info(err) + } + ret.authzMetadataCopy = *ret.authzMetadata + ret.writeAuthzMetadataToDB(authzVersionFld, ret.authzMetadata.AuthzVersion) + ret.writeAuthzMetadataToDB(authzCreatedOnFld, ret.authzMetadata.AuthzCreatedOn) + return ret +} + +// Rotate implements the gNSI.authz.Rotate RPC. +func (srv *GNSIAuthzServer) Rotate(stream authz.Authz_RotateServer) error { + ctx := stream.Context() + log.Infof("GNSI Authz Rotate RPC") + _, err := authenticateFunc(srv.config, ctx, "gnoi", false) + if err != nil { + log.Errorf("authentication failed in Rotate RPC: %v", err) + return err + } + session := time.Now().Nanosecond() + // Concurrent Authz RPCs are not allowed. + if !authzMu.TryLock() { + log.V(0).Infof("[%v]gNSI: authz.Rotate already in use", session) + return status.Errorf(codes.Aborted, "concurrent authz.Rotate RPCs are not allowed") + } + defer authzMu.Unlock() + + log.V(2).Infof("[%v]gNSI: Begin authz.Rotate", session) + defer log.V(2).Infof("[%v]gNSI: End authz.Rotate", session) + + srv.checkpointAuthzFreshness() + if err := srv.checkpointAuthzFile(); err != nil { + log.V(0).Infof("Failure during Authz checkpoint: %v", err) + } + for { + req, err := stream.Recv() + if err == io.EOF { + log.V(0).Infof("[%v]gNSI: Received unexpected EOF", session) + // Connection closed without Finalize message. Revert all changes made until now. + if err := copyFile(srv.config.AuthzPolicyFile+backupExt, srv.config.AuthzPolicyFile); err != nil { + log.V(0).Infof("[%v]gnsi: failed to revert authz policy file (%v): %v", session, srv.config.AuthzPolicyFile, err) + } + srv.revertAuthzFileFreshness() + return status.Errorf(codes.Aborted, "No Finalize message") + } + if err != nil { + log.V(0).Infof("[%v]gnsi: while processing a rotate request got error: `%v`. Reverting to last good state.", session, err) + // Connection closed without Finalize message. Revert all changes made until now. + if err := copyFile(srv.config.AuthzPolicyFile+backupExt, srv.config.AuthzPolicyFile); err != nil { + log.V(0).Infof("[%v]gnsi: failed to revert authz policy file (%v): %v", session, srv.config.AuthzPolicyFile, err) + } + srv.revertAuthzFileFreshness() + return status.Errorf(codes.Aborted, err.Error()) + } + if endReq := req.GetFinalizeRotation(); endReq != nil { + // This is the last message. All changes are final. + log.V(2).Infof("[%v]gNSI: Received Finalize: %v", session, endReq) + srv.commitAuthzFileChanges() + srv.saveAuthzFileFreshess(srv.config.AuthzMetaFile) + return nil + } + resp, err := srv.processRotateRequest(req) + if err != nil { + log.V(0).Infof("[%v]gnsi: while processing a rotate request got error: `%v`. Reverting to last good state.", session, err) + // Connection closed without Finalize message. Revert all changes made until now. + if err := copyFile(srv.config.AuthzPolicyFile+backupExt, srv.config.AuthzPolicyFile); err != nil { + log.V(0).Infof("[%v]gnsi: failed to revert authz policy file (%v): %v", session, srv.config.AuthzPolicyFile, err) + } + srv.revertAuthzFileFreshness() + return err + } + if err := stream.Send(resp); err != nil { + log.V(0).Infof("[%v]gnsi: while processing a rotate request got error: `%v`. Reverting to last good state.", session, err) + // Connection closed without Finalize message. Revert all changes made until now. + if err := copyFile(srv.config.AuthzPolicyFile+backupExt, srv.config.AuthzPolicyFile); err != nil { + log.V(0).Infof("[%v]gnsi: failed to revert authz policy file (%v): %v", session, srv.config.AuthzPolicyFile, err) + } + srv.revertAuthzFileFreshness() + return status.Errorf(codes.Aborted, err.Error()) + } + } +} + +func (srv *GNSIAuthzServer) processRotateRequest(req *authz.RotateAuthzRequest) (*authz.RotateAuthzResponse, error) { + policyReq := req.GetUploadRequest() + if policyReq == nil { + return nil, status.Errorf(codes.Aborted, `Unknown request: "%v"`, req) + } + log.V(2).Infof("received a gNSI.Authz UploadRequest request message") + log.V(3).Infof("request message: %v", policyReq) + if len(policyReq.GetPolicy()) == 0 { + return nil, status.Errorf(codes.Aborted, "Authz policy cannot be empty!") + } + if len(policyReq.GetVersion()) == 0 { + return nil, status.Errorf(codes.Aborted, "Authz policy version cannot be empty!") + } + if !json.Valid([]byte(policyReq.GetPolicy())) { + return nil, status.Errorf(codes.Aborted, "Authz policy `%v` is malformed", policyReq.GetPolicy()) + } + if err := fileCheck(srv.config.AuthzPolicyFile); err != nil { + return nil, status.Errorf(codes.NotFound, "Error in reading file %s: %v. Please try Install.", srv.config.AuthzPolicyFile, err) + } + if srv.gnsiAuthz.authzMetadata.AuthzVersion == policyReq.GetVersion() && !req.GetForceOverwrite() { + return nil, status.Errorf(codes.AlreadyExists, "Authz with version `%v` already exists", policyReq.GetVersion()) + } + if err := srv.writeAuthzMetadataToDB(authzVersionFld, policyReq.GetVersion()); err != nil { + return nil, status.Errorf(codes.Aborted, err.Error()) + } + if err := srv.writeAuthzMetadataToDB(authzCreatedOnFld, strconv.FormatUint(policyReq.GetCreatedOn(), 10)); err != nil { + return nil, status.Errorf(codes.Aborted, err.Error()) + } + if err := srv.saveToAuthzFile(policyReq.GetPolicy()); err != nil { + return nil, status.Errorf(codes.Aborted, err.Error()) + } + resp := &authz.RotateAuthzResponse{ + RotateResponse: &authz.RotateAuthzResponse_UploadResponse{}, + } + return resp, nil +} + +func (srv *GNSIAuthzServer) saveToAuthzFile(p string) error { + tmpDst, err := os.CreateTemp(filepath.Dir(srv.config.AuthzPolicyFile), filepath.Base(srv.config.AuthzPolicyFile)) + if err != nil { + return err + } + if _, err := tmpDst.Write([]byte(p)); err != nil { + if e := os.Remove(tmpDst.Name()); e != nil { + log.V(1).Infof("Failed to cleanup file: %v: %v", tmpDst.Name(), e) + } + return err + } + if err := tmpDst.Close(); err != nil { + if e := os.Remove(tmpDst.Name()); e != nil { + log.V(1).Infof("Failed to cleanup file: %v: %v", tmpDst.Name(), e) + } + return err + } + if err := os.Rename(tmpDst.Name(), srv.config.AuthzPolicyFile); err != nil { + if e := os.Remove(tmpDst.Name()); e != nil { + log.V(1).Infof("Failed to cleanup file: %v: %v", tmpDst.Name(), e) + } + return err + } + return os.Chmod(srv.config.AuthzPolicyFile, 0600) +} + +func (srv *GNSIAuthzServer) checkpointAuthzFile() error { + log.V(2).Infof("Checkpoint authz file: %v", srv.config.AuthzPolicyFile) + return copyFile(srv.config.AuthzPolicyFile, srv.config.AuthzPolicyFile+backupExt) +} + +func (srv *GNSIAuthzServer) commitAuthzFileChanges() error { + // Check if the active policy file exists. + srcStat, err := os.Stat(srv.config.AuthzPolicyFile) + if err != nil { + return err + } + if !srcStat.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file", srv.config.AuthzPolicyFile) + } + // OK. Now the backup can be deleted. + backup := srv.config.AuthzPolicyFile + backupExt + backupStat, err := os.Stat(backup) + if err != nil { + // Already does not exist. + return nil + } + if !backupStat.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file; did not remove it.", backup) + } + return os.Remove(backup) +} + +// writeAuthzMetadataToDB writes the credentials freshness data to the DB. +func (srv *GNSIAuthzServer) writeAuthzMetadataToDB(fld, val string) error { + if err := writeCredentialsMetadataToDB(authzP4rtTbl, "", fld, val); err != nil { + return err + } + if err := writeCredentialsMetadataToDB(authzGnxiTbl, "", fld, val); err != nil { + return err + } + switch fld { + case authzVersionFld: + srv.authzMetadata.AuthzVersion = val + case authzCreatedOnFld: + srv.authzMetadata.AuthzCreatedOn = val + } + return nil +} + +type AuthzMetadata struct { + AuthzVersion string `json:"authz_version"` + AuthzCreatedOn string `json:"authz_created_on"` +} + +func NewAuthzMetadata() *AuthzMetadata { + return &AuthzMetadata{ + AuthzVersion: "unknown", + AuthzCreatedOn: "0", + } +} + +func (srv *GNSIAuthzServer) checkpointAuthzFreshness() { + log.V(2).Infof("checkpoint authz freshness") + srv.authzMetadataCopy = *srv.authzMetadata +} + +func (srv *GNSIAuthzServer) revertAuthzFileFreshness() { + log.V(2).Infof("revert authz freshness") + srv.writeAuthzMetadataToDB(authzVersionFld, srv.authzMetadataCopy.AuthzVersion) + srv.writeAuthzMetadataToDB(authzCreatedOnFld, srv.authzMetadataCopy.AuthzCreatedOn) +} + +func (srv *GNSIAuthzServer) saveAuthzFileFreshess(path string) error { + log.V(2).Infof("save authz metadata to file: %v", path) + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + if err := enc.Encode(*srv.authzMetadata); err != nil { + log.V(0).Info(err) + return err + } + if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil { + if e := os.Remove(path); e != nil { + err = fmt.Errorf("Write %s failed: %w; Cleanup failed", path, err) + } + return err + } + return nil +} + +func (srv *GNSIAuthzServer) loadAuthzFreshness(path string) error { + log.V(2).Infof("load authz metadata from file: %v", path) + bytes, err := os.ReadFile(path) + if err != nil { + return err + } + return json.Unmarshal(bytes, srv.authzMetadata) +} diff --git a/gnmi_server/gnsi_authz_test.go b/gnmi_server/gnsi_authz_test.go new file mode 100644 index 000000000..484f8f3b0 --- /dev/null +++ b/gnmi_server/gnsi_authz_test.go @@ -0,0 +1,924 @@ +package gnmi + +import ( + "context" + "crypto/tls" + "fmt" + "github.com/openconfig/gnsi/authz" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" + "io" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" +) + +var ( + TestAuthzPolicyFile string // Global variable to hold policy path + TestAuthzMetaFile string // Global variable to hold meta path +) + +const ( + // Authz is a location of the Authz Policy + authzTestPolicyFile = "../testdata/gnsi/authz_policy.json" + authzTestMetaFile = "../testdata/gnsi/authz_meta.json" +) + +var authzRotationTestCases = []struct { + desc string + f func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) +}{ + { + desc: "RotateOpenClose", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + // 1) Close connection without sending any message. + stream.CloseSend() + // 2) Receive error reporting premature closure of the stream. + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error reporting premature closure of the stream.") + } + if status.Code(err) != codes.Aborted { + t.Fatalf("Unexpected error: %v", err) + } + }, + }, + { + desc: "RotateStreamRecvError", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + // We use a context with a short timeout, and then cancel it later to trigger the error. + shortCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + stream, err := sc.Rotate(shortCtx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + + // 1) Send a valid policy upload request to move the server past the auth check + // and into the main `for` loop. This also creates a backup file. + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: generateVersion(), + CreatedOn: generateCreatedOn(), + Policy: authzTestPolicyFileV2, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + + // 2) Receive the confirmation response. + resp, err := stream.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if cfm := resp.GetUploadResponse(); cfm == nil { + t.Fatal("Did not receive expected UploadResponse response") + } + + // 3) Cancel the client-side context. + cancel() + + // 4) Attempt to receive the next message. This should fail with a non-EOF error. + _, err = stream.Recv() + + // We expect a non-nil error. + if err == nil { + t.Fatal("Expected an error (e.g., context canceled) from stream.Recv()") + } + + // The server wraps the `stream.Recv()` error and returns it with codes.Aborted. + // The original error inside the server will be a gRPC/context error (e.g., codes.Canceled or DeadlineExceeded). + if status.Code(err) != codes.Canceled { + t.Fatalf("Expected codes.Canceled error from client due to context cancellation, got: %v", status.Code(err)) + } + }, + }, + { + desc: "RotateStreamSendError", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + const testPort = 8081 + // 0) Create a temporary, separate connection just for this test, as we must close it prematurely. + tlsConfig := &tls.Config{InsecureSkipVerify: true} + cred := &loginCreds{Username: testUsername, Password: testPassword} + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + grpc.WithPerRPCCredentials(cred), + } + targetAddr := fmt.Sprintf("127.0.0.1:%d", testPort) + + conn, err := grpc.Dial(targetAddr, opts...) + if err != nil { + t.Fatalf("Dialing to %s failed: %v", targetAddr, err) + } + // NOTE: Defer conn.Close() is NOT used as we close it manually. + + tempClient := authz.NewAuthzClient(conn) + + // Open the streaming RPC. + stream, err := tempClient.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + conn.Close() + t.Fatal(err.Error()) + } + + // 1) Send a valid policy upload request. This will cause the server to process it + // and attempt to send the response (`stream.Send(resp)`). + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: generateVersion(), + CreatedOn: generateCreatedOn(), + Policy: authzTestPolicyFileV2, + }, + }, + }) + if err != nil { + conn.Close() + t.Fatal(err.Error()) + } + + // 2) Immediately close the underlying client connection. + // This guarantees the server's subsequent stream.Send(resp) will fail with a transport error, + // triggering the desired coverage block. + if err := conn.Close(); err != nil { + t.Fatalf("Failed to close connection: %v", err) + } + + // 3) Attempt to receive confirmation. This call will fail due to the closed connection. + // The failure of the server's Send operation (unseen directly by the client) + // covers the target lines in the server's Rotate RPC. + if _, err := stream.Recv(); err == nil { + t.Fatal("Expected an error (connection closed) but received a successful response.") + } + }, + }, + { + desc: "RotatePolicyEmptyRequest", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + // 1) Generate a rotation request and send it to the switch. + err = stream.Send(&authz.RotateAuthzRequest{}) + if err != nil { + t.Fatal(err.Error()) + } + // 2) Receive error reporting premature closure of the stream. + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error reporting premature closure of the stream.") + } + if status.Code(err) != codes.Aborted { + t.Fatalf("Unexpected error: %v", err) + } + // And sanity check + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV1) + }, + }, + { + desc: "RotatePolicyEmptyUploadRequest", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + // 1) Generate a rotation request and send it to the switch. + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{}, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + // 2) Receive error reporting premature closure of the stream. + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error reporting premature closure of the stream.") + } + if status.Code(err) != codes.Aborted { + t.Fatalf("Unexpected error: %v", err) + } + // And sanity check + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV1) + }, + }, + { + desc: "RotatePolicyWrongJSON", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + // 1) Generate a rotation request and send it to the switch. + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: generateVersion(), + CreatedOn: generateCreatedOn(), + Policy: string(`{"key":}`), + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + // 2) Receive error reporting premature closure of the stream. + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error reporting premature closure of the stream.") + } + if status.Code(err) != codes.Aborted { + t.Fatalf("Unexpected error: %v", err) + } + // And sanity check + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV1) + }, + }, + { + desc: "RotatePolicyNoVersion", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + // 1) Generate a rotation request and send it to the switch. + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + CreatedOn: generateCreatedOn(), + Policy: string(`{}`), + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + // 2) Receive error reporting premature closure of the stream. + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error reporting premature closure of the stream.") + } + if status.Code(err) != codes.Aborted { + t.Fatalf("Unexpected error: %v", err) + } + // And sanity check + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV1) + }, + }, + { + desc: "RotatePolicySuccess", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + // 1) Generate a authz policy and send it to the switch. + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: generateVersion(), + CreatedOn: generateCreatedOn(), + Policy: authzTestPolicyFileV2, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + // 2) Receive confirmation that the certificate was accepted. + resp, err := stream.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if cfm := resp.GetUploadResponse(); cfm == nil { + t.Fatal("Did not receive expected UploadResponse response") + } + // 3) Check if the credentials are pointed to by the links. + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV2) + // 4) Finalize the operation by sending the Finalize message. + if err = stream.Send(&authz.RotateAuthzRequest{RotateRequest: &authz.RotateAuthzRequest_FinalizeRotation{}}); err != nil { + t.Fatal(err.Error()) + } + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error") + } + if err != io.EOF { + t.Fatalf("Unexpected error: %v", err) + } + }, + }, + { + desc: "RotatePolicyNoFinalize", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + // 1) Generate a authz policy and send it to the switch. + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: generateVersion(), + CreatedOn: generateCreatedOn(), + Policy: authzTestPolicyFileV2, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + // 2) Receive confirmation that the certificate was accepted. + resp, err := stream.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if cfm := resp.GetUploadResponse(); cfm == nil { + t.Fatal("Did not receive expected UploadResponse response") + } + // 3) Check if the credentials are pointed to by the links. + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV2) + // 4) Close connection without sending any message. + stream.CloseSend() + // 5) Receive error reporting premature closure of the stream. + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error reporting premature closure of the stream.") + } + if status.Code(err) != codes.Aborted { + t.Fatalf("Unexpected error: %v", err) + } + }, + }, + { + desc: "RotateTheSamePolicyTwice", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + ver := generateVersion() + createdOn := generateCreatedOn() + // 1) Generate a authz policy and send it to the switch. + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: ver, + CreatedOn: createdOn, + Policy: authzTestPolicyFileV2, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + // 2) Receive confirmation that the certificate was accepted. + resp, err := stream.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if cfm := resp.GetUploadResponse(); cfm == nil { + t.Fatal("Did not receive expected UploadResponse response") + } + // 3) Check if the credentials are pointed to by the links. + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV2) + // 4) Finalize the operation by sending the Finalize message. + if err = stream.Send(&authz.RotateAuthzRequest{RotateRequest: &authz.RotateAuthzRequest_FinalizeRotation{}}); err != nil { + t.Fatal(err.Error()) + } + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error") + } + if err != io.EOF { + t.Fatalf("Unexpected error: %v", err) + } + // 5) Send the same authz policy to the switch. + stream, err = sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: ver, + CreatedOn: createdOn, + Policy: authzTestPolicyFileV2, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + // 6) Receive confirmation that the certificate was rejected. + if _, err := stream.Recv(); status.Code(err) != codes.AlreadyExists { + t.Fatalf("Unexpected error: %v", err) + } + // 7) Check if the credentials are pointed to by the links. + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV2) + }, + }, + { + desc: "RotateTheSamePolicyTwiceWithForceOverwrite", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + ver := generateVersion() + createdOn := generateCreatedOn() + // 1) Generate a authz policy and send it to the switch. + req := &authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: ver, + CreatedOn: createdOn, + Policy: authzTestPolicyFileV2, + }, + }, + ForceOverwrite: true, + } + err = stream.Send(req) + if err != nil { + t.Fatal(err.Error()) + } + // 2) Receive confirmation that the certificate was accepted. + resp, err := stream.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if cfm := resp.GetUploadResponse(); cfm == nil { + t.Fatal("Did not receive expected UploadResponse response") + } + // 3) Check if the credentials are pointed to by the links. + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV2) + // 4) Finalize the operation by sending the Finalize message. + if err = stream.Send(&authz.RotateAuthzRequest{RotateRequest: &authz.RotateAuthzRequest_FinalizeRotation{}}); err != nil { + t.Fatal(err.Error()) + } + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error") + } + if err != io.EOF { + t.Fatalf("Unexpected error: %v", err) + } + // 5) Send the same authz policy to the switch. + stream, err = sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + err = stream.Send(req) + if err != nil { + t.Fatal(err.Error()) + } + // 6) Receive confirmation that the certificate was accepted. + resp, err = stream.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if cfm := resp.GetUploadResponse(); cfm == nil { + t.Fatal("Did not receive expected UploadResponse response") + } + // 7) Check if the credentials are pointed to by the links. + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV2) + // 8) Finalize the operation by sending the Finalize message. + if err = stream.Send(&authz.RotateAuthzRequest{RotateRequest: &authz.RotateAuthzRequest_FinalizeRotation{}}); err != nil { + t.Fatal(err.Error()) + } + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error") + } + if err != io.EOF { + t.Fatalf("Unexpected error: %v", err) + } + }, + }, + { + desc: "ParallelRotationCalls", + f: func(ctx context.Context, t *testing.T, sc authz.AuthzClient, s *Server) { + // 0) Open the streaming RPC. + stream, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + ver := generateVersion() + createdOn := generateCreatedOn() + // 1) Generate a authz policy and send it to the switch. + err = stream.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: ver, + CreatedOn: createdOn, + Policy: authzTestPolicyFileV2, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + // 2) Receive confirmation that the certificate was accepted. + resp, err := stream.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if cfm := resp.GetUploadResponse(); cfm == nil { + t.Fatal("Did not receive expected UploadResponse response") + } + // 3) Check if the credentials are pointed to by the links. + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV2) + // 4) Attempt to send the same authz policy to the switch. + stream2, err := sc.Rotate(ctx, grpc.EmptyCallOption{}) + if err != nil { + t.Fatal(err.Error()) + } + err = stream2.Send(&authz.RotateAuthzRequest{ + RotateRequest: &authz.RotateAuthzRequest_UploadRequest{ + UploadRequest: &authz.UploadRequest{ + Version: ver, + CreatedOn: createdOn, + Policy: authzTestPolicyFileV2, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + // 5) Receive information that the certificate was rejected. + if _, err = stream2.Recv(); err == nil { + t.Fatal("Expected an error") + } + if status.Code(err) != codes.Aborted { + t.Fatalf("Unexpected error: %v", err) + } + // 6) Finalize the operation by sending the Finalize message. + if err = stream.Send(&authz.RotateAuthzRequest{RotateRequest: &authz.RotateAuthzRequest_FinalizeRotation{}}); err != nil { + t.Fatal(err.Error()) + } + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error") + } + if err != io.EOF { + t.Fatalf("Unexpected error: %v", err) + } + // 7) Check if the credentials are pointed to by the links. + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV2) + }, + }, +} + +// TestGnsiAuthzRotation tests implementation of gnsi.authz rotate server. +func TestGnsiAuthzRotation(t *testing.T) { + + // Set the configuration paths globally. + TestAuthzPolicyFile = authzTestPolicyFile + TestAuthzMetaFile = authzTestMetaFile + + const testPort = 8081 + s := createAuthServer(t, testPort) + + defer os.Remove(authzTestPolicyFile) + go runServer(t, s) + defer s.Stop() + + // Create a gNSI.authz client and connect it to the gNSI.authz server. + tlsConfig := &tls.Config{InsecureSkipVerify: true} + + // Use dummy credentials for the client + cred := &loginCreds{Username: testUsername, Password: testPassword} + + // Attach both TLS transport and the PerRPC BasicAuth credentials + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + grpc.WithPerRPCCredentials(cred), + } + + targetAddr := fmt.Sprintf("127.0.0.1:%d", testPort) + orig := authenticateFunc + defer func() { authenticateFunc = orig }() + authenticateFunc = func(config *Config, ctx context.Context, target string, writeAccess bool) (context.Context, error) { + return ctx, nil + } + + var mu sync.Mutex + for _, tc := range authzRotationTestCases { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + t.Run(tc.desc, func(t *testing.T) { + mu.Lock() + defer mu.Unlock() + conn, err := grpc.Dial(targetAddr, opts...) + if err != nil { + cancel() + t.Fatalf("Dialing to %s failed: %v", targetAddr, err) + } + defer conn.Close() + sc := authz.NewAuthzClient(conn) + tc.f(ctx, t, sc, s) + if err := resetAuthzPolicyFile(s.config); err != nil { + t.Errorf("Error when reverting to V1: %v", err) + } + // And sanity check + expectPolicyMatch(t, authzTestPolicyFile, authzTestPolicyFileV1) + }) + cancel() + } + s.gnsiAuthz.saveAuthzFileFreshess(s.config.AuthzMetaFile) +} + +// TestGnsiAuthzRotateUnauthenticated tests implementation of gnsi.authz Rotate Unsuthenticated error. +func TestGnsiAuthzRotateUnauthenticated(t *testing.T) { + const testPort = 8083 // Use a different port to avoid conflict + s := createAuthServer(t, testPort) + go runServer(t, s) + defer s.Stop() + + // Create gNSI.authz client + tlsConfig := &tls.Config{InsecureSkipVerify: true} + cred := &loginCreds{Username: testUsername, Password: testPassword} + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + grpc.WithPerRPCCredentials(cred), + } + targetAddr := fmt.Sprintf("127.0.0.1:%d", testPort) + conn, err := grpc.Dial(targetAddr, opts...) + if err != nil { + t.Fatalf("Dialing to %s failed: %v", targetAddr, err) + } + defer conn.Close() + noCredsClient := authz.NewAuthzClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + // 1) Open the streaming RPC using the client *without* credentials. + stream, err := noCredsClient.Rotate(ctx, grpc.EmptyCallOption{}) + + // Check if the server immediately rejected the connection due to missing credentials. + // If the error is not nil, we check the status code. + if err != nil { + if status.Code(err) != codes.Unauthenticated { + t.Fatalf("Expected Unauthenticated error on stream creation, got: %v (code: %v)", err, status.Code(err)) + } + return // Authentication failed as expected. + } + + // 2) If the stream successfully opened, the server's authentication + // will fail upon the first `Recv()`. We close the send stream to get the final error. + stream.CloseSend() + + // 3) Receive error reporting authentication failure. + if _, err = stream.Recv(); err == nil { + t.Fatal("Expected an error due to authentication failure.") + } + + if status.Code(err) != codes.Unauthenticated { + t.Fatalf("Expected Unauthenticated error, got: %v (code: %v)", err, status.Code(err)) + } + s.gnsiAuthz.saveAuthzFileFreshess(s.config.AuthzMetaFile) + +} + +// TestGnsiAuthzUnimplemented tests implementation of gnsi.authz Probe and Get server. +func TestGnsiAuthzUnimplemented(t *testing.T) { + const testPort = 8082 // Use a different port to avoid conflict + s := createAuthServer(t, testPort) + go runServer(t, s) + defer s.Stop() + + // Create gNSI.authz client + tlsConfig := &tls.Config{InsecureSkipVerify: true} + cred := &loginCreds{Username: testUsername, Password: testPassword} + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + grpc.WithPerRPCCredentials(cred), + } + targetAddr := fmt.Sprintf("127.0.0.1:%d", testPort) + conn, err := grpc.Dial(targetAddr, opts...) + if err != nil { + t.Fatalf("Dialing to %s failed: %v", targetAddr, err) + } + defer conn.Close() + sc := authz.NewAuthzClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + // --- Test Probe RPC --- + t.Run("ProbeUnimplemented", func(t *testing.T) { + _, err := sc.Probe(ctx, &authz.ProbeRequest{}) + if status.Code(err) != codes.Unimplemented { + t.Fatalf("Probe() returned unexpected error code: got %v, want %v", status.Code(err), codes.Unimplemented) + } + }) + + // --- Test Get RPC --- + t.Run("GetUnimplemented", func(t *testing.T) { + _, err := sc.Get(ctx, &authz.GetRequest{}) + if status.Code(err) != codes.Unimplemented { + t.Fatalf("Get() returned unexpected error code: got %v, want %v", status.Code(err), codes.Unimplemented) + } + }) +} +func TestSaveToAuthzFile_Errors(t *testing.T) { + tests := []struct { + name string + setupConfig func(dir string) string + policy string + wantErr bool + }{ + { + name: "CreateTemp_Error_InvalidDir", + setupConfig: func(dir string) string { + // Path in a non-existent directory + return filepath.Join(dir, "missing", "policy.json") + }, + policy: "{}", + wantErr: true, + }, + { + name: "Rename_Error_IsDirectory", + setupConfig: func(dir string) string { + path := filepath.Join(dir, "policy.json") + // Create a directory at the target path. + // os.Rename from file to directory usually fails on Unix. + os.MkdirAll(path, 0755) + return path + }, + policy: "{}", + wantErr: true, + }, + { + name: "Chmod_Error_MissingFile", + setupConfig: func(dir string) string { + // This is a bit of a hack: we use a path that is valid for + // creation but we will make the directory read-only later. + return filepath.Join(dir, "readonly", "policy.json") + }, + policy: "{}", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, _ := os.MkdirTemp("", "authz-err-*") + defer os.RemoveAll(tmpDir) + + filePath := tt.setupConfig(tmpDir) + + // For the Chmod/Rename cases, sometimes we need to restrict the parent dir + if tt.name == "Rename_Error_IsDirectory" || tt.name == "Chmod_Error_MissingFile" { + // On some systems, making the parent dir 0555 (Read/Execute only) + // prevents modifications like Rename or Chmod. + os.Chmod(tmpDir, 0555) + defer os.Chmod(tmpDir, 0755) // Clean up so RemoveAll works + } + + srv := &GNSIAuthzServer{ + Server: &Server{ + config: &Config{AuthzPolicyFile: filePath}, + }, + } + + err := srv.saveToAuthzFile(tt.policy) + if (err != nil) != tt.wantErr { + t.Errorf("saveToAuthzFile() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} +func TestCommitAuthzFileChanges_Errors(t *testing.T) { + const backupExt = ".bak" // Ensure this matches your package's internal constant + + tests := []struct { + name string + setup func(tmpDir string) (string, func()) + wantErr string + }{ + { + name: "MainFile_DoesNotExist", + setup: func(tmpDir string) (string, func()) { + // Return a path that was never created + return filepath.Join(tmpDir, "missing_policy.json"), nil + }, + wantErr: "no such file or directory", + }, + { + name: "MainFile_IsDirectory", + setup: func(tmpDir string) (string, func()) { + path := filepath.Join(tmpDir, "policy_dir") + os.Mkdir(path, 0755) + return path, nil + }, + wantErr: "is not a regular file", + }, + { + name: "BackupFile_IsDirectory", + setup: func(tmpDir string) (string, func()) { + path := filepath.Join(tmpDir, "policy.json") + os.WriteFile(path, []byte("data"), 0644) + // Create a directory where the backup file should be + os.Mkdir(path+backupExt, 0755) + return path, nil + }, + wantErr: "is not a regular file; did not remove it", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "commit-test-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + policyPath, cleanup := tt.setup(tmpDir) + if cleanup != nil { + defer cleanup() + } + + srv := &GNSIAuthzServer{ + Server: &Server{ + config: &Config{AuthzPolicyFile: policyPath}, + }, + } + + err = srv.commitAuthzFileChanges() + if err == nil { + t.Error("expected error but got nil") + } else if !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("error %q does not contain expected string %q", err.Error(), tt.wantErr) + } + }) + } +} + +func expectPolicyMatch(t *testing.T, path, src string) { + t.Helper() + dst, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if src != string(dst) { + t.Fatalf("want golden:\n%v\ngot %s:\n%v", src, path, string(dst)) + } +} + +func generateCreatedOn() uint64 { + return uint64(time.Now().UnixNano()) +} +func resetAuthzPolicyFile(config *Config) error { + return attemptWrite(config.AuthzPolicyFile, []byte(authzTestPolicyFileV1), 0600) +} + +const authzTestPolicyFileV1 = `{ + "name": "policy_file_1", + "allow_rules": [ + { + "name": "allow_all" + } + ], + "audit_logging_options": { + "audit_condition": "ON_DENY_AND_ALLOW", + "audit_loggers": [ + { + "name": "authz_logger", + "is_optional": false + } + ] + } +}` +const authzTestPolicyFileV2 = `{ + "name": "policy_file_2", + "allow_rules": [ + { + "name": "allow_all" + } + ], + "audit_logging_options": { + "audit_condition": "ON_DENY_AND_ALLOW", + "audit_loggers": [ + { + "name": "authz_logger", + "is_optional": false + } + ] + } +}` diff --git a/gnmi_server/gnsi_util.go b/gnmi_server/gnsi_util.go new file mode 100644 index 000000000..a41540ff8 --- /dev/null +++ b/gnmi_server/gnsi_util.go @@ -0,0 +1,60 @@ +package gnmi + +import ( + "fmt" + "io" + "os" + "path/filepath" + + log "github.com/golang/glog" +) + +func copyFile(srcPath, dstPath string) error { + srcStat, err := os.Stat(srcPath) + if err != nil { + return err + } + if !srcStat.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file", srcPath) + } + src, err := os.Open(srcPath) + if err != nil { + return err + } + defer src.Close() + // os.CreateTemp requires the directory to exist. filepath.Dir(dstPath) must be a valid directory. + tmpDst, err := os.CreateTemp(filepath.Dir(dstPath), filepath.Base(dstPath)) + if err != nil { + return err + } + if _, err := io.Copy(tmpDst, src); err != nil { + if e := os.Remove(tmpDst.Name()); e != nil { + log.V(2).Infof("Failed to cleanup file: %v: %v", tmpDst.Name(), e) + } + return err + } + if err := tmpDst.Close(); err != nil { + if e := os.Remove(tmpDst.Name()); e != nil { + log.V(2).Infof("Failed to cleanup file: %v: %v", tmpDst.Name(), e) + } + return err + } + if err := os.Rename(tmpDst.Name(), dstPath); err != nil { + if e := os.Remove(tmpDst.Name()); e != nil { + log.V(2).Infof("Failed to cleanup file: %v: %v", tmpDst.Name(), e) + } + return err + } + return os.Chmod(dstPath, 0600) +} + +func fileCheck(f string) error { + srcStat, err := os.Lstat(f) // Use os.Lstat to check the file itself, not its target if it's a symlink. + if err != nil { + return err + } + if !srcStat.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file", f) + } + return nil +} diff --git a/gnmi_server/gnsi_util_test.go b/gnmi_server/gnsi_util_test.go new file mode 100644 index 000000000..e1ed626be --- /dev/null +++ b/gnmi_server/gnsi_util_test.go @@ -0,0 +1,254 @@ +package gnmi + +import ( + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" // Corrected import path for external builds +) + +// --- Test copyFile --- +func TestCopyFile(t *testing.T) { + tests := []struct { + name string + srcContent string + srcFile string + dstFile string + makeSrc bool + makeSrcDir bool + wantErr bool + wantDstContent string + wantDstMode os.FileMode + }{ + { + name: "Success", + srcContent: "test content", + srcFile: "src.txt", + dstFile: "dst.txt", + makeSrc: true, + wantErr: false, + wantDstContent: "test content", + wantDstMode: 0600, + }, + { + name: "SrcNotExist", + srcFile: "nonexistent.txt", + dstFile: "dst.txt", + makeSrc: false, + wantErr: true, + }, + { + name: "SrcIsNotRegularFile", + srcFile: "srcDir", + dstFile: "dst.txt", + makeSrc: true, + makeSrcDir: true, + wantErr: true, + }, + { + name: "DstParentNotExist", + srcContent: "test", + srcFile: "src.txt", + dstFile: "subdir/dst.txt", + makeSrc: true, + wantErr: true, // Expect error because "subdir" doesn't exist + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + subTmpDir := t.TempDir() // Create a unique temp directory for each subtest + + srcPath := filepath.Join(subTmpDir, tt.srcFile) + dstPath := filepath.Join(subTmpDir, tt.dstFile) + + if tt.makeSrc { + if tt.makeSrcDir { + if err := os.MkdirAll(srcPath, 0755); err != nil { + t.Fatalf("Failed to create src directory: %v", err) + } + } else { + if err := os.MkdirAll(filepath.Dir(srcPath), 0755); err != nil { + t.Fatalf("Failed to create parent dir for src: %v", err) + } + if err := os.WriteFile(srcPath, []byte(tt.srcContent), 0644); err != nil { + t.Fatalf("Failed to create src file: %v", err) + } + } + } + + // This block ensures the destination parent exists ONLY for cases where no error is expected. + if !tt.wantErr { + dstParent := filepath.Dir(dstPath) + if dstParent != subTmpDir { + if err := os.MkdirAll(dstParent, 0755); err != nil { + t.Fatalf("Failed to create parent dir for dst: %v", err) + } + } + } + + err := copyFile(srcPath, dstPath) + + if tt.wantErr { + if err == nil { + t.Errorf("copyFile(%q, %q) got nil error, want error", tt.srcFile, tt.dstFile) + } + // Verify destination file was not created. os.Stat should return an error. + if _, statErr := os.Stat(dstPath); statErr == nil { + t.Errorf("copyFile(%q, %q) destination file exists, but should not on error", tt.srcFile, tt.dstFile) + } + } else { + if err != nil { + t.Errorf("copyFile(%q, %q) got error %v, want nil", tt.srcFile, tt.dstFile, err) + } + gotContent, err := os.ReadFile(dstPath) + if err != nil { + t.Fatalf("Failed to read destination file: %v", err) + } + if diff := cmp.Diff(tt.wantDstContent, string(gotContent)); diff != "" { + t.Errorf("copyFile(%q, %q) content mismatch (-want +got):\n%s", tt.srcFile, tt.dstFile, diff) + } + stat, err := os.Stat(dstPath) + if err != nil { + t.Fatalf("Failed to stat destination file: %v", err) + } + if got, want := stat.Mode().Perm(), os.FileMode(0600); got != want { + t.Errorf("copyFile(%q, %q) got permissions %v, want %v", tt.srcFile, tt.dstFile, got, want) + } + } + }) + } +} + +func TestCopyFile_Errors(t *testing.T) { + // Create a temp directory for the test + tmpDir := t.TempDir() + srcPath := filepath.Join(tmpDir, "source.txt") + dstPath := filepath.Join(tmpDir, "dest.txt") + + // Setup valid source + os.WriteFile(srcPath, []byte("data"), 0644) + + t.Run("OpenError", func(t *testing.T) { + dirPath := t.TempDir() + err := copyFile("/dev/full", dstPath) + err = copyFile(dirPath, dstPath) + if err == nil { + t.Error("Expected error for non-regular file") + } + }) + + t.Run("RenameError", func(t *testing.T) { + // Line 42-46: Attempt to rename to a directory that doesn't exist + // or where the user lacks write permissions. + invalidDst := "/proc/invalid-path/dest" + + err := copyFile(srcPath, invalidDst) + if err == nil { + t.Error("expected error during os.Rename, got nil") + } + }) + t.Run("RenameError_and_Cleanup", func(t *testing.T) { + srcPath := filepath.Join(t.TempDir(), "src.txt") + os.WriteFile(srcPath, []byte("data"), 0644) + + // To make Rename fail: use a destination that is an existing directory + invalidDst := t.TempDir() + + err := copyFile(srcPath, invalidDst) + if err == nil { + t.Error("expected error during os.Rename to a directory") + } + }) + t.Run("CopyFailure", func(t *testing.T) { + srcPath := filepath.Join(t.TempDir(), "src.txt") + os.WriteFile(srcPath, []byte("test"), 0644) + + // Use a destination path where CreateTemp succeeds but writing fails + // Often achieved by pointing to a directory with a strict quota or + // a filesystem that goes Read-Only. + err := copyFile(srcPath, "/dev/null/invalid") + if err == nil { + t.Errorf("Expected failure") + } + }) +} + +// --- Test fileCheck --- +func TestFileCheck(t *testing.T) { + tests := []struct { + name string + filePath string + setup func(string, string) error // Function to set up the file/dir, takes tmpDir and filePath + wantErr bool + }{ + { + name: "RegularFile", + filePath: "regular.txt", + setup: func(tmp, p string) error { return os.WriteFile(p, []byte("test"), 0644) }, + wantErr: false, + }, + { + name: "NonExistentFile", + filePath: "nonexistent.txt", + setup: func(tmp, p string) error { return nil }, + wantErr: true, + }, + { + name: "IsDirectory", + filePath: "a_directory", + setup: func(tmp, p string) error { return os.Mkdir(p, 0755) }, + wantErr: true, + }, + { + name: "SymlinkToFile", + filePath: "link.txt", + setup: func(tmp, p string) error { + targetPath := filepath.Join(tmp, "target.txt") + if err := os.WriteFile(targetPath, []byte("data"), 0644); err != nil { + return err + } + return os.Symlink(targetPath, p) + }, + wantErr: true, // os.Lstat will show this is a symlink, not a regular file. + }, + { + name: "SymlinkToDir", + filePath: "link_to_dir", + setup: func(tmp, p string) error { + targetPath := filepath.Join(tmp, "target_dir") + if err := os.Mkdir(targetPath, 0755); err != nil { + return err + } + return os.Symlink(targetPath, p) + }, + wantErr: true, // os.Lstat will show this is a symlink, not a regular file. + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + subTmpDir := t.TempDir() + fullPath := filepath.Join(subTmpDir, tt.filePath) + + if tt.setup != nil { + if err := tt.setup(subTmpDir, fullPath); err != nil { + t.Fatalf("Failed to setup test case: %v", err) + } + } + + err := fileCheck(fullPath) + + if tt.wantErr { + if err == nil { + t.Errorf("fileCheck(%q) got nil error, want error", tt.filePath) + } + } else { + if err != nil { + t.Errorf("fileCheck(%q) got error %v, want nil", tt.filePath, err) + } + } + }) + } +} diff --git a/gnmi_server/server.go b/gnmi_server/server.go index 5932830cc..c18b88cc9 100644 --- a/gnmi_server/server.go +++ b/gnmi_server/server.go @@ -38,11 +38,13 @@ import ( gnoi_file_pb "github.com/openconfig/gnoi/file" gnoi_healthz_pb "github.com/openconfig/gnoi/healthz" gnoi_os_pb "github.com/openconfig/gnoi/os" + gnsi_authz_pb "github.com/openconfig/gnsi/authz" gnsi_certz_pb "github.com/openconfig/gnsi/certz" gnoi_debug "github.com/sonic-net/sonic-gnmi/pkg/gnoi/debug" gnoi_debug_pb "github.com/sonic-net/sonic-gnmi/proto/gnoi/debug" testcert "github.com/sonic-net/sonic-gnmi/testdata/tls" "google.golang.org/grpc" + "google.golang.org/grpc/authz" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/tls/certprovider" "google.golang.org/grpc/peer" @@ -56,6 +58,12 @@ var ( supportedEncodings = []gnmipb.Encoding{gnmipb.Encoding_JSON, gnmipb.Encoding_JSON_IETF, gnmipb.Encoding_PROTO} ) +// Path to the `/var/log/telemetry-con` directory on the `host` side. +const ( + authLogPath = "/host_var/log/messages" + authzRefreshingInterval = 5 * time.Second +) + // Server manages a single gNMI Server implementation. Each client that connects // via Subscribe or Get will receive a stream of updates based on the requested // path. Set request is processed by server too. @@ -82,7 +90,9 @@ type Server struct { masterEID uint128 gnoi_system_pb.UnimplementedSystemServer factory_reset.UnimplementedFactoryResetServer - gnsiCertz *GNSICertzServer + gnsiCertz *GNSICertzServer + authzWatcher *authz.FileWatcherInterceptor + gnsiAuthz *GNSIAuthzServer } // handleOperationalGet handles OPERATIONAL target requests directly with standard gNMI types @@ -203,16 +213,19 @@ type Config struct { ImgDir string GetOptions func(*Config) ([]grpc.ServerOption, []certprovider.Provider, error) // gnsi.certz mTLS flags - CaCertLnk string // Path to symlink pointing to current CA certificate. - SrvCertLnk string // Path to symlink pointing to current server's certificate. - SrvKeyLnk string // Path to symlink pointing to current server's private key. - CaCertFile string // Path to the first CA certificate. - SrvCertFile string // Path to the first server's certificate. - SrvKeyFile string // Path to the first server's private key. - CertCRLConfig string // Path to the CRL directory. Disable if empty. - IntManFile string // Path to the Integrity Manifest file. - CertzMetaFile string // Path to JSON file with gRPC credential metadata. - FedPolicyFile string // Path to federation policy file. + CaCertLnk string // Path to symlink pointing to current CA certificate. + SrvCertLnk string // Path to symlink pointing to current server's certificate. + SrvKeyLnk string // Path to symlink pointing to current server's private key. + CaCertFile string // Path to the first CA certificate. + SrvCertFile string // Path to the first server's certificate. + SrvKeyFile string // Path to the first server's private key. + CertCRLConfig string // Path to the CRL directory. Disable if empty. + IntManFile string // Path to the Integrity Manifest file. + CertzMetaFile string // Path to JSON file with gRPC credential metadata. + FedPolicyFile string // Path to federation policy file. + AuthzPolicy bool // Enable authz policy. + AuthzPolicyFile string // Path to JSON file with authz policies. + AuthzMetaFile string // Path to JSON file with authz metadata. } // DBusOSBackend is a concrete implementation of OSBackend @@ -303,10 +316,11 @@ func (i AuthTypes) Unset(mode string) error { // registerAllServices registers all gNMI and gNOI services on the given gRPC server. func registerAllServices(s *grpc.Server, srv *Server, fileSrv *FileServer, osSrv *OSServer, containerzSrv *ContainerzServer, - debugSrv *DebugServer, healthzSrv *HealthzServer, certzSrv *GNSICertzServer) { + debugSrv *DebugServer, healthzSrv *HealthzServer, certzSrv *GNSICertzServer, authzSrv *GNSIAuthzServer) { gnmipb.RegisterGNMIServer(s, srv) factory_reset.RegisterFactoryResetServer(s, srv) gnsi_certz_pb.RegisterCertzServer(s, certzSrv) + gnsi_authz_pb.RegisterAuthzServer(s, authzSrv) spb_jwt_gnoi.RegisterSonicJwtServiceServer(s, srv) if srv.config.EnableTranslibWrite || srv.config.EnableNativeWrite { gnoi_system_pb.RegisterSystemServer(s, srv) @@ -470,13 +484,33 @@ func NewServer(config *Config, tlsOpts []grpc.ServerOption, commonOpts []grpc.Se var providers []certprovider.Provider common_utils.InitCounters() + // Set authorization policy. + var authzWatcher *authz.FileWatcherInterceptor + if config.AuthzPolicy { + authzWatcher, err := authz.NewFileWatcher(config.AuthzPolicyFile, authzRefreshingInterval) + if err != nil { + return nil, err + } else { + commonOpts = append(commonOpts, grpc.ChainStreamInterceptor( + authzWatcher.StreamInterceptor)) + commonOpts = append(commonOpts, grpc.ChainUnaryInterceptor( + authzWatcher.UnaryInterceptor)) + } + } + + s := grpc.NewServer(commonOpts...) + reflection.Register(s) + srv := &Server{ config: config, clients: map[string]*Client{}, certProviders: providers, SaveStartupConfig: saveOnSetDisabled, - ReqFromMaster: ReqFromMasterDisabledMA, - masterEID: uint128{High: 0, Low: 0}, + // ReqFromMaster point to a function that is called to verify if + // the request comes from a master controller. + ReqFromMaster: ReqFromMasterDisabledMA, + masterEID: uint128{High: 0, Low: 0}, + authzWatcher: authzWatcher, } // Create service servers (shared between TCP and UDS) @@ -489,6 +523,9 @@ func NewServer(config *Config, tlsOpts []grpc.ServerOption, commonOpts []grpc.Se } containerzSrv := &ContainerzServer{server: srv} healthzSrv := &HealthzServer{Server: srv} + authzSrv := NewGNSIAuthzServer(srv) + srv.gnsiAuthz = authzSrv + readWhitelist, writeWhitelist := gnoi_debug.ConstructWhitelists() debugSrv := &DebugServer{ Server: srv, @@ -511,7 +548,7 @@ func NewServer(config *Config, tlsOpts []grpc.ServerOption, commonOpts []grpc.Se return nil, fmt.Errorf("failed to open listener port %d: %v", config.Port, err) } - registerAllServices(srv.s, srv, fileSrv, osSrv, containerzSrv, debugSrv, healthzSrv, certzSrv) + registerAllServices(srv.s, srv, fileSrv, osSrv, containerzSrv, debugSrv, healthzSrv, certzSrv, authzSrv) } // UDS Server (UnixSocket set) @@ -547,7 +584,7 @@ func NewServer(config *Config, tlsOpts []grpc.ServerOption, commonOpts []grpc.Se srv.udsListener = nil srv.udsServer = nil } else { - registerAllServices(srv.udsServer, srv, fileSrv, osSrv, containerzSrv, debugSrv, healthzSrv, certzSrv) + registerAllServices(srv.udsServer, srv, fileSrv, osSrv, containerzSrv, debugSrv, healthzSrv, certzSrv, authzSrv) } } diff --git a/gnmi_server/server_test.go b/gnmi_server/server_test.go index 8657870f4..085440623 100644 --- a/gnmi_server/server_test.go +++ b/gnmi_server/server_test.go @@ -258,8 +258,14 @@ func createAuthServer(t *testing.T, port int64) *Server { cfg := &Config{ Port: port, EnableTranslibWrite: true, - UserAuth: AuthTypes{"password": true, "cert": true, "jwt": true}, - ImgDir: "/tmp", + UserAuth: AuthTypes{ + "password": true, + "cert": true, + "jwt": true, + }, + ImgDir: "/tmp", + AuthzMetaFile: TestAuthzMetaFile, + AuthzPolicyFile: TestAuthzPolicyFile, } s, err := NewServer(cfg, tlsOpts, nil) if err != nil { diff --git a/go.mod b/go.mod index b551afe02..48741acc4 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/c9s/goprocinfo v0.0.0-20191125144613-4acdd056c72d github.com/dgrijalva/jwt-go v3.2.1-0.20210802184156-9742bd7fca1c+incompatible github.com/fsnotify/fsnotify v1.4.7 + github.com/go-redis/redis/v7 v7.4.1 github.com/godbus/dbus/v5 v5.1.0 github.com/gogo/protobuf v1.3.2 github.com/golang/glog v1.2.4 @@ -30,7 +31,7 @@ require ( golang.org/x/net v0.38.0 google.golang.org/grpc v1.69.2 google.golang.org/grpc/security/advancedtls v1.0.0 - google.golang.org/protobuf v1.36.6 + google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/sh/v3 v3.8.0 @@ -43,9 +44,11 @@ require ( github.com/bgentry/speakeasy v0.1.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/go-redis/redis/v7 v7.0.0-beta.3.0.20190824101152-d19aba07b476 // indirect + github.com/envoyproxy/go-control-plane v0.12.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/onsi/ginkgo v1.10.3 // indirect @@ -60,6 +63,7 @@ require ( golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect ) diff --git a/go.sum b/go.sum index 791d77a79..d444bf6db 100644 --- a/go.sum +++ b/go.sum @@ -1380,6 +1380,7 @@ github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230428030218-4003588d1b74/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -1398,6 +1399,7 @@ github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJ github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= +github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= @@ -1406,6 +1408,7 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -1440,6 +1443,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-redis/redis/v7 v7.0.0-beta.3.0.20190824101152-d19aba07b476 h1:WNSiFp8Ww4ZP7XUzW56zDYv5roKQ4VfsdHCLoh8oDj4= github.com/go-redis/redis/v7 v7.0.0-beta.3.0.20190824101152-d19aba07b476/go.mod h1:xhhSbUMTsleRPur+Vgx9sUHtyN33bdjxY+9/0n9Ig8s= +github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= +github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= @@ -1641,9 +1646,11 @@ github.com/msteinert/pam v0.0.0-20201130170657-e61372126161 h1:XQ1+fYPzaWZCVdu1x github.com/msteinert/pam v0.0.0-20201130170657-e61372126161/go.mod h1:np1wUFZ6tyoke22qDJZY40URn9Ae51gX7ljIWXN5TJs= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/openconfig/gnmi v0.0.0-20200617225440-d2b4e6a45802 h1:WXFwJlWOJINlwlyAZuNo4GdYZS6qPX36+rRUncLmN8Q= @@ -1845,6 +1852,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -2343,6 +2351,7 @@ google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917/go.mod h1:pZqR+glS google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -2371,6 +2380,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go. google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c= @@ -2425,6 +2435,7 @@ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFW google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 8c859d025..187b0a5cb 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -72,6 +72,9 @@ type TelemetryConfig struct { IntManFile *string CertzMetaFile *string ImgDirPath *string + AuthzMetaFile *string + AuthPolicyEnabled *bool + AuthzPolicyFile *string } func main() { @@ -194,6 +197,9 @@ func setupFlags(fs *flag.FlagSet) (*TelemetryConfig, *gnmi.Config, error) { IntManFile: fs.String("integrity_manifest_file", "", "Full path name of integrity manifest file."), CertCRLConfig: fs.String("cert_crl_dir", "/mtls/crl", "Directory for CRL files"), CertzMetaFile: fs.String("grpc_meta", "/keys/grpc-version.json", "gRPC credentials metadata JSON file"), + AuthzMetaFile: fs.String("authz_meta", "/keys/authz-version.json", "authz policy metadata JSON file"), + AuthPolicyEnabled: fs.Bool("authz_policy_enabled", false, "Enable authz policy. Require insecure flag to be false."), + AuthzPolicyFile: fs.String("authorization_policy_file", "/keys/authorization_policy.json", "Full path name of the JSON authorization policy file."), } fs.Var(&telemetryCfg.UserAuth, "client_auth", "Client auth mode(s) - none,cert,password") @@ -305,6 +311,9 @@ func setupFlags(fs *flag.FlagSet) (*TelemetryConfig, *gnmi.Config, error) { log.V(2).Info("client_auth mode cert requires ca_crt option. Disabling cert mode authentication.") } + cfg.AuthzMetaFile = string(*telemetryCfg.AuthzMetaFile) + cfg.AuthzPolicy = *telemetryCfg.AuthPolicyEnabled && !*telemetryCfg.Insecure + cfg.AuthzPolicyFile = string(*telemetryCfg.AuthzPolicyFile) return telemetryCfg, cfg, nil } diff --git a/testdata/gnsi/authz_meta.json b/testdata/gnsi/authz_meta.json new file mode 100644 index 000000000..4151d5fe3 --- /dev/null +++ b/testdata/gnsi/authz_meta.json @@ -0,0 +1 @@ +{"authz_version":"1763718231444414746","authz_created_on":"1763718231444419137"}