From 018ca5dd7d19dd2e8933e64537102463f2064a88 Mon Sep 17 00:00:00 2001 From: Neha Das Date: Tue, 10 Feb 2026 17:23:23 +0000 Subject: [PATCH] Implements the frontend logic for gNSI Credentialz Signed-off-by: Pattela JAYARAGINI Signed-off-by: Niranjani Vivek --- gnmi_server/gnsi_credentialz.go | 1020 +++++++++ gnmi_server/gnsi_credentialz_test.go | 2955 ++++++++++++++++++++++++++ gnmi_server/server.go | 15 +- go.mod | 23 +- go.sum | 42 +- testdata/gnsi/console-version.json | 1 + testdata/gnsi/ssh-version.json | 1 + 7 files changed, 4009 insertions(+), 48 deletions(-) create mode 100644 gnmi_server/gnsi_credentialz.go create mode 100644 gnmi_server/gnsi_credentialz_test.go create mode 100644 testdata/gnsi/console-version.json create mode 100644 testdata/gnsi/ssh-version.json diff --git a/gnmi_server/gnsi_credentialz.go b/gnmi_server/gnsi_credentialz.go new file mode 100644 index 000000000..3b4450dc1 --- /dev/null +++ b/gnmi_server/gnsi_credentialz.go @@ -0,0 +1,1020 @@ +package gnmi + +import ( + "bytes" + "context" + b64 "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "os" + "strconv" + "strings" + "sync" + "time" + + ssc "github.com/sonic-net/sonic-gnmi/sonic_service_client" + + log "github.com/golang/glog" + credz "github.com/openconfig/gnsi/credentialz" + "github.com/redis/go-redis/v9" + "github.com/sonic-net/sonic-gnmi/common_utils" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" +) + +var ( + credzMu sync.Mutex +) + +const ( + sshAccountTbl string = "SSH_ACCOUNT" + sshHostTbl string = "SSH_HOST" + consoleAccountTbl string = "CONSOLE_ACCOUNT" + sshKeysVersionFld string = "keys_version" + sshKeysCreatedOnFld string = "keys_created_on" + sshPrincipalsVersionFld string = "principals_version" + sshPrincipalsCreatedOnFld string = "principals_created_on" + sshCaKeysVersionFld string = "ca_keys_version" + sshCaKeysCreatedOnFld string = "ca_keys_created_on" + consolePasswordVersionFld string = "password_version" + consolePasswordCreatedOnFld string = "password_created_on" + + // glomeConfigRedisKey is GLOME config's Redis key appended to credentialsTbl. + glomeConfigRedisKey = "GLOME_CONFIG" +) + +type cpType int + +const ( + consoleCP cpType = iota + sshCP +) + +var ( + sshKeyTypePrefix = map[credz.KeyType]string{ + credz.KeyType_KEY_TYPE_UNSPECIFIED: "unspecified", + credz.KeyType_KEY_TYPE_ECDSA_P_256: "ecdsa-sha2-nistp256", + credz.KeyType_KEY_TYPE_ECDSA_P_521: "ecdsa-sha2-nistp521", + credz.KeyType_KEY_TYPE_ED25519: "ssh-ed25519", + credz.KeyType_KEY_TYPE_RSA_2048: "ssh-rsa", + credz.KeyType_KEY_TYPE_RSA_4096: "ssh-rsa", + } +) + +type GNSICredentialzServer struct { + *Server + sshCredMetadata *SshCredMetadata + sshCredMetadataCopy SshCredMetadata + consoleCredMetadata *ConsoleCredMetadata + consoleCredMetadataCopy ConsoleCredMetadata + // glomeConfigMetadata is used to store rollback data for STATE_DB + glomeConfigMetadata *GlomeConfigMetadata + stateDbClient *redis.Client + credz.UnimplementedCredentialzServer +} + +func NewGNSICredentialzServer(srv *Server) *GNSICredentialzServer { + stateDbClient, err := common_utils.GetRedisDBClient() + if err != nil { + log.V(0).Infof("Failed to create STATE_DB client: %v", err) + } + + ret := &GNSICredentialzServer{ + Server: srv, + sshCredMetadata: NewSshCredMetadata(), + consoleCredMetadata: NewConsoleCredMetadata(), + stateDbClient: stateDbClient, + } + + // Load the SSH Creds from host OS to update the STATE_DB with the switch system's current state. + // The switch needs to be supplied with the initial SSH keys (gnetch keys) before gNSI is up and running. + if err := ret.loadCredentialFreshness(srv.config.SshCredMetaFile); err != nil { + log.V(0).Infof("srv.config.SshCredMetaFile=%s error=%v", srv.config.SshCredMetaFile, err) + } + if err := ret.loadConsoleCredentialFreshness(srv.config.ConsoleCredMetaFile); err != nil { + log.V(0).Infof("srv.config.ConsoleCredMetaFile=%s error=%v", srv.config.ConsoleCredMetaFile, err) + } + ret.sshCredMetadataCopy = *ret.sshCredMetadata + ret.writeSshHostCredentialsMetadataToDB(sshCaKeysVersionFld, ret.sshCredMetadata.Host.CaKeysVersion) + ret.writeSshHostCredentialsMetadataToDB(sshCaKeysCreatedOnFld, ret.sshCredMetadata.Host.CaKeysCreatedOn) + for a, u := range ret.sshCredMetadata.Accounts { + ret.writeSshAccountCredentialsMetadataToDB(a, sshKeysVersionFld, u.KeysVersion) + ret.writeSshAccountCredentialsMetadataToDB(a, sshKeysCreatedOnFld, u.KeysCreatedOn) + ret.writeSshAccountCredentialsMetadataToDB(a, sshPrincipalsVersionFld, u.UsersVersion) + ret.writeSshAccountCredentialsMetadataToDB(a, sshPrincipalsCreatedOnFld, u.UsersCreatedOn) + } + ret.consoleCredMetadataCopy = *ret.consoleCredMetadata + for a, u := range ret.consoleCredMetadata.Accounts { + ret.writeConsoleAccountCredentialsMetadataToDB(a, consolePasswordVersionFld, u.PasswordVersion) + ret.writeConsoleAccountCredentialsMetadataToDB(a, consolePasswordCreatedOnFld, u.PasswordCreatedOn) + } + + // Initialize the Glome config metadata from STATE_DB data to use as rollback data. + ret.InitGlomeConfigMetadata(context.Background()) + + return ret +} + +// Close cleans up the GNSICredentialzServer such as closing initialized clients. +func (srv *GNSICredentialzServer) Close() { + if srv.stateDbClient != nil { + if err := srv.stateDbClient.Close(); err != nil { + log.Errorf("Error closing STATE_DB client: %v", err) + } + } +} + +func (srv *GNSICredentialzServer) CanGenerateKey(context.Context, *credz.CanGenerateKeyRequest) (*credz.CanGenerateKeyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CanGenerateKey not implemented") +} + +func (srv *GNSICredentialzServer) GetPublicKeys(context.Context, *credz.GetPublicKeysRequest) (*credz.GetPublicKeysResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPublicKeys not implemented") +} + +// RotateAccountCredentials implements corresponding RPC +func (srv *GNSICredentialzServer) RotateAccountCredentials(stream credz.Credentialz_RotateAccountCredentialsServer) error { + ctx := stream.Context() + ctx, err := authenticate(srv.config, ctx, "gnsi", false) + if err != nil { + return err + } + // Concurrent Rotate{Account|Host} RPCs are not allowed. + if !credzMu.TryLock() { + log.V(0).Infoln("Concurrent Rotate{Account|Host} RPCs are not allowed.") + return status.Errorf(codes.Aborted, "Concurrent Rotate{Account|Host} RPCs are not allowed.") + } + defer credzMu.Unlock() + + log.V(2).Info("gNSI: credz.RotateAccountCredentials") + + consoleBackup := false + sshBackup := false + + for { + req, err := stream.Recv() + if err != nil { + log.V(0).Infoln("Received error: ", err) + if sshBackup { + srv.checkpointRestore(sshCP) + } + if consoleBackup { + srv.checkpointRestore(consoleCP) + } + return status.Errorf(codes.Aborted, "%v", err) + } + + var resp *credz.RotateAccountCredentialsResponse + + switch r := req.GetRequest().(type) { + case *credz.RotateAccountCredentialsRequest_Finalize: + if endReq := req.GetFinalize(); endReq != nil { + // This is the last message. All changes are final. + log.V(2).Infof("Received a Finalize request message %v", endReq) + if sshBackup { + srv.checkpointDelete(sshCP) + } + if consoleBackup { + srv.checkpointDelete(consoleCP) + } + return nil + } + return status.Errorf(codes.Aborted, "%v", err) + case *credz.RotateAccountCredentialsRequest_Password: + log.V(2).Info("Received a Password request") + if !consoleBackup { + consoleBackup = true + if err := srv.checkpointCreate(consoleCP); err != nil { + return err + } + defer srv.saveConsoleCredentialsFreshness(srv.config.ConsoleCredMetaFile) + } + resp, err = srv.processConsolePassword(req) + case *credz.RotateAccountCredentialsRequest_Credential: + if !sshBackup { + sshBackup = true + if err := srv.checkpointCreate(sshCP); err != nil { + return err + } + defer srv.saveCredentialsFreshness(srv.config.SshCredMetaFile) + } + resp, err = srv.processSshCred(req) + case *credz.RotateAccountCredentialsRequest_User: + if !sshBackup { + sshBackup = true + if err := srv.checkpointCreate(sshCP); err != nil { + return err + } + defer srv.saveCredentialsFreshness(srv.config.SshCredMetaFile) + } + resp, err = srv.processSshUser(req) + default: + return status.Errorf(codes.Aborted, "Unknown Request: %+v", r) + } + if err != nil { + log.V(0).Infof("Failed to process request: %v", err) + if sshBackup { + srv.checkpointRestore(sshCP) + } + if consoleBackup { + srv.checkpointRestore(consoleCP) + } + return status.Errorf(codes.Aborted, "%v", err) + } + log.V(2).Info("Finished process request") + if err := stream.Send(resp); err != nil { + if sshBackup { + srv.checkpointRestore(sshCP) + } + if consoleBackup { + srv.checkpointRestore(consoleCP) + } + return status.Errorf(codes.Aborted, "%v", err) + } + } +} + +// RotateHostParameters implements corresponding RPC +func (srv *GNSICredentialzServer) RotateHostParameters(stream credz.Credentialz_RotateHostParametersServer) error { + ctx := stream.Context() + ctx, err := authenticate(srv.config, ctx, "gnsi", false) + if err != nil { + return err + } + + // Concurrent Rotate{Account|Host}Credentials RPCs are not allowed. + if !credzMu.TryLock() { + return status.Errorf(codes.Aborted, "Concurrent Mutate{Account|Host}Credentials RPCs are not allowed.") + } + defer credzMu.Unlock() + log.V(2).Info("gNSI: credz.RotateHostParameters") + + // Main loop to process requests from the stream. + // - Before request handler is called, no checkpoint is created thus there is + // nothing to rollback. The checkpoint is managed by each request handler. + // - After request handler returns, it implies that the transaction is completed + // (request and Finalize are processed). Thus, there is nothing to rollback. + // - After a valid flow of requests are processed, close the stream. The valid + // flows are defined in: https://github.com/openconfig/gnsi/blob/main/credentialz/credentialz.proto). + for { + req, err := stream.Recv() + if err != nil { + return status.Errorf(codes.Aborted, "%v", err) + } + + // handlerErr is the error returned by the handler functions (e.g. handleGlome, etc). + var handlerErr error + switch r := req.GetRequest().(type) { + case *credz.RotateHostParametersRequest_SshCaPublicKey: + handlerErr = srv.handleSshCaPublicKey(ctx, stream, req) + case *credz.RotateHostParametersRequest_Glome: + handlerErr = srv.handleGlome(ctx, stream, req) + case *credz.RotateHostParametersRequest_ServerKeys: + return status.Errorf(codes.Unimplemented, "ServerKeys Unimplemented") + case *credz.RotateHostParametersRequest_GenerateKeys: + return status.Errorf(codes.Unimplemented, "GenerateKeys Unimplemented") + case *credz.RotateHostParametersRequest_AuthenticationAllowed: + return status.Errorf(codes.Unimplemented, "AuthenticationAllowed Unimplemented") + case *credz.RotateHostParametersRequest_AuthorizedPrincipalCheck: + return status.Errorf(codes.Unimplemented, "AuthorizedPrincipalCheck Unimplemented") + case *credz.RotateHostParametersRequest_Finalize: + return status.Errorf(codes.Aborted, "Finalize cannot be the first message in a transaction.") + default: + return status.Errorf(codes.Aborted, "Unknown Request: %+v", r) + } + + if handlerErr != nil { + return handlerErr + } + log.V(2).Info("Rotation request is completed successfully.") + return nil + } +} + +// handleSshCaPublicKey handles the SSH CA public key request. +// It creates a checkpoint, processes the SSH CA public key, sends the response +// to the client, and expects a Finalize message. +func (srv *GNSICredentialzServer) handleSshCaPublicKey(ctx context.Context, stream credz.Credentialz_RotateHostParametersServer, req *credz.RotateHostParametersRequest) error { + if err := srv.checkpointCreate(sshCP); err != nil { + return err + } + defer srv.saveCredentialsFreshness(srv.config.SshCredMetaFile) + + resp, err := srv.processSshCaPublicKey(req) + if err != nil { + srv.checkpointRestore(sshCP) + return status.Errorf(codes.Aborted, "%v", err) + } + + // Send the response to the client. + if err := stream.Send(resp); err != nil { + srv.checkpointRestore(sshCP) + return status.Errorf(codes.Aborted, "%v", err) + } + + // Expect a Finalize message. + req, err = stream.Recv() + if err != nil { + srv.checkpointRestore(sshCP) + return status.Errorf(codes.Aborted, "%v", err) + } + if _, ok := req.GetRequest().(*credz.RotateHostParametersRequest_Finalize); !ok { + srv.checkpointRestore(sshCP) + return status.Errorf(codes.Aborted, "Expected Finalize message, but received %T", req.GetRequest()) + } + log.V(2).Info("Received a Finalize request message for SshCaPublicKey.") + srv.checkpointDelete(sshCP) + return nil +} + +// handleGlome handles the Glome request. It creates a checkpoint, processes the Glome config, +// sends the response to the client, and expects a Finalize message. +func (srv *GNSICredentialzServer) handleGlome(ctx context.Context, stream credz.Credentialz_RotateHostParametersServer, req *credz.RotateHostParametersRequest) error { + lastUpdated := time.Now().UnixNano() + resp, err := srv.processGlomeConfig(ctx, req, lastUpdated) + if err != nil { + srv.glomeCheckpointRestore(ctx) + return status.Errorf(codes.Aborted, "%v", err) + } + + // Send the response to the client. + if err := stream.Send(resp); err != nil { + srv.glomeCheckpointRestore(ctx) + return status.Errorf(codes.Aborted, "%v", err) + } + + // Expect a Finalize message. + req, err = stream.Recv() + if err != nil { + srv.glomeCheckpointRestore(ctx) + return status.Errorf(codes.Aborted, "%v", err) + } + if _, ok := req.GetRequest().(*credz.RotateHostParametersRequest_Finalize); !ok { + srv.glomeCheckpointRestore(ctx) + return status.Errorf(codes.Aborted, "Expected Finalize message, but received %T", req.GetRequest()) + } + log.V(2).Info("Received a Finalize request message for Glome.") + return nil +} + +func (srv *GNSICredentialzServer) checkpointCreate(source cpType) error { + log.V(3).Infof("Checkpoint Create: %v", source) + sc, err := ssc.NewDbusClientProvider() + if err != nil { + return err + } + defer sc.Close() + if source == sshCP { + log.V(3).Info("Creating SSH Checkpoint") + err := sc.SSHCheckpoint(ssc.CredzCPCreate) + if err != nil { + return status.Errorf(codes.Internal, "Cannot start the ssh transaction:%v", err) + } + srv.checkpointSshCredentialFreshness() + } + if source == consoleCP { + err := sc.ConsoleCheckpoint(ssc.CredzCPCreate) + if err != nil { + return status.Errorf(codes.Internal, "Cannot start the console transaction:%v", err) + } + srv.checkpointConsoleFreshness() + } + return nil +} + +func (srv *GNSICredentialzServer) checkpointRestore(source cpType) { + log.V(3).Infof("Checkpoint Restore: %v", source) + sc, _ := ssc.NewDbusClientProvider() + if source == sshCP { + if err := sc.SSHCheckpoint(ssc.CredzCPRestore); err != nil { + log.V(0).Infof("Could not restore from checkpoint: %v", err) + } + srv.revertSshCredentialFreshness() + } + if source == consoleCP { + if err := sc.ConsoleCheckpoint(ssc.CredzCPRestore); err != nil { + log.V(0).Infof("Could not restore from checkpoint: %v", err) + } + srv.revertConsoleCredentialFreshness() + } +} + +// glomeCheckpointRestore rolls back the Glome configuration file in the host OS system, +// and also rollbacks the STATE_DB to the checkpoint state. +func (srv *GNSICredentialzServer) glomeCheckpointRestore(ctx context.Context) { + log.V(3).Infof("Glome Checkpoint Restore with context: %v", ctx) + dbusClient, _ := ssc.NewDbusClientProvider() + if err := dbusClient.GLOMERestoreCheckpoint(ctx); err != nil { + log.V(0).Infof("Could not restore from GLOME checkpoint: %v", err) + } + + // Restore the Glome Config in STATE_DB to the checkpoint. + srv.writeGlomeConfigMetadataToStateDB(ctx, srv.glomeConfigMetadata) +} + +func (srv *GNSICredentialzServer) checkpointDelete(source cpType) { + log.V(3).Infof("Checkpoint Delete: %v", source) + sc, _ := ssc.NewDbusClientProvider() + if source == sshCP { + if err := sc.SSHCheckpoint(ssc.CredzCPDelete); err != nil { + log.V(1).Infof("Could not delete checkpoint: %v", err) + } + } + if source == consoleCP { + if err := sc.ConsoleCheckpoint(ssc.CredzCPDelete); err != nil { + log.V(1).Infof("Could not delete checkpoint: %v", err) + } + } +} + +func (srv *GNSICredentialzServer) processSshCred(req *credz.RotateAccountCredentialsRequest) (*credz.RotateAccountCredentialsResponse, error) { + credReq := req.GetCredential() + // Sanity checks. + if len(credReq.GetCredentials()) == 0 { + return nil, fmt.Errorf("credentials cannot be empty") + } + for _, set := range credReq.GetCredentials() { + if set.GetVersion() == "" { + return nil, fmt.Errorf("version cannot be empty") + } + if set.GetCreatedOn() == 0 { + return nil, fmt.Errorf("created_on cannot be empty") + } + if len(set.GetAccount()) == 0 { + return nil, fmt.Errorf("account cannot be empty") + } + if len(set.GetAuthorizedKeys()) == 0 { + return nil, fmt.Errorf("authorized_keys cannot be empty") + } + } + // Build the message to be sent to the back-end. + var b strings.Builder + fmt.Fprintf(&b, `{ "SshAccountKeys": [ `) + for i, set := range credReq.GetCredentials() { + fmt.Fprintf(&b, `{ "account": "%s", "keys": [`, set.Account) + for i, key := range set.AuthorizedKeys { + fmt.Fprintf(&b, ` { "key" : "%s %s %s", "options" : [`, sshKeyTypePrefix[key.GetKeyType()], b64.StdEncoding.EncodeToString(key.AuthorizedKey), key.Description) + for i, o := range key.Options { + fmt.Fprintf(&b, ` { "name" : "%v", "value": "%v" }`, o.GetName(), o.GetValue()) + if i < len(key.Options)-1 { + fmt.Fprintf(&b, `,`) + } + } + fmt.Fprintf(&b, ` ] }`) + if i < len(set.AuthorizedKeys)-1 { + fmt.Fprintf(&b, `,`) + } + } + fmt.Fprintf(&b, ` ] }`) + if i < len(credReq.GetCredentials())-1 { + fmt.Fprintf(&b, `,`) + } + } + fmt.Fprintf(&b, ` ] }`) + sc, err := ssc.NewDbusClientProvider() + if err != nil { + return nil, err + } + defer sc.Close() + if err := sc.SSHMgmtSet(b.String()); err != nil { + return nil, err + } + for _, set := range credReq.GetCredentials() { + if err := srv.writeSshAccountCredentialsMetadataToDB(set.Account, sshKeysVersionFld, set.GetVersion()); err != nil { + return nil, err + } + if err := srv.writeSshAccountCredentialsMetadataToDB(set.Account, sshKeysCreatedOnFld, strconv.FormatUint(set.GetCreatedOn(), 10)); err != nil { + return nil, err + } + } + resp := &credz.RotateAccountCredentialsResponse{ + Response: &credz.RotateAccountCredentialsResponse_Credential{}, + } + return resp, nil +} + +func (srv *GNSICredentialzServer) processSshUser(req *credz.RotateAccountCredentialsRequest) (*credz.RotateAccountCredentialsResponse, error) { + usrReq := req.GetUser() + // Sanity checks. + if len(usrReq.GetPolicies()) == 0 { + return nil, fmt.Errorf("policies cannot be empty") + } + for _, set := range usrReq.GetPolicies() { + if set.GetVersion() == "" { + return nil, fmt.Errorf("version cannot be empty") + } + if set.GetCreatedOn() == 0 { + return nil, fmt.Errorf("created_on cannot be empty") + } + if len(set.GetAccount()) == 0 { + return nil, fmt.Errorf("account cannot be empty") + } + if len(set.GetAuthorizedPrincipals().GetAuthorizedPrincipals()) == 0 { + return nil, fmt.Errorf("authorized_principals cannot be empty") + } + } + // Build the message to be sent to the back-end. + var b strings.Builder + fmt.Fprintf(&b, `{ "SshAccountUsers": [`) + for i, set := range usrReq.GetPolicies() { + fmt.Fprintf(&b, ` { "account": "%s", "users": [`, set.Account) + for j, user := range set.GetAuthorizedPrincipals().GetAuthorizedPrincipals() { + fmt.Fprintf(&b, ` { "name" : "%v", "options" : [`, user.GetAuthorizedUser()) + for k, o := range user.Options { + fmt.Fprintf(&b, ` { "name" : "%v", "value": "%v" }`, o.GetName(), o.GetValue()) + if k < len(user.Options)-1 { + fmt.Fprintf(&b, `,`) + } + } + fmt.Fprintf(&b, ` ] }`) + if j < len(set.GetAuthorizedPrincipals().GetAuthorizedPrincipals())-1 { + fmt.Fprintf(&b, `,`) + } + } + fmt.Fprintf(&b, ` ] }`) + if i < len(usrReq.GetPolicies())-1 { + fmt.Fprintf(&b, `,`) + } + } + fmt.Fprintf(&b, ` ] }`) + sc, err := ssc.NewDbusClientProvider() + if err != nil { + return nil, err + } + defer sc.Close() + if err := sc.SSHMgmtSet(b.String()); err != nil { + return nil, err + } + for _, set := range usrReq.GetPolicies() { + if err := srv.writeSshAccountCredentialsMetadataToDB(set.Account, sshPrincipalsVersionFld, set.GetVersion()); err != nil { + return nil, err + } + if err := srv.writeSshAccountCredentialsMetadataToDB(set.Account, sshPrincipalsCreatedOnFld, strconv.FormatUint(set.GetCreatedOn(), 10)); err != nil { + return nil, err + } + } + resp := &credz.RotateAccountCredentialsResponse{ + Response: &credz.RotateAccountCredentialsResponse_User{}, + } + return resp, nil +} + +func (srv *GNSICredentialzServer) processConsolePassword(req *credz.RotateAccountCredentialsRequest) (*credz.RotateAccountCredentialsResponse, error) { + credReq := req.GetPassword() + // Sanity checks. + if len(credReq.GetAccounts()) == 0 { + return nil, fmt.Errorf("list of username/password pairs cannot be empty") + } + for _, set := range credReq.GetAccounts() { + if set.GetVersion() == "" { + return nil, fmt.Errorf("version cannot be empty") + } + if set.GetCreatedOn() == 0 { + return nil, fmt.Errorf("created_on cannot be empty") + } + if set.Account == "" { + return nil, fmt.Errorf("name cannot be empty") + } + pwd := set.GetPassword() + if pwd == nil { + return nil, fmt.Errorf("password cannot be empty") + } + if pwd.GetPlaintext() == "" { + return nil, fmt.Errorf("password must be plaintext; CryptoHash unimplemented") + } + } + + // Build a message to be sent to the back-end. + var b strings.Builder + fmt.Fprintf(&b, `{ "ConsolePasswords": [ `) + for i, set := range credReq.GetAccounts() { + fmt.Fprintf(&b, `{ "name": "%s", "password" : "%s" }`, set.GetAccount(), set.GetPassword().GetPlaintext()) + if i < len(credReq.GetAccounts())-1 { + fmt.Fprintf(&b, `,`) + } + } + fmt.Fprintf(&b, ` ] }`) + sc, err := ssc.NewDbusClientProvider() + if err != nil { + return nil, err + } + defer sc.Close() + if err := sc.ConsoleSet(b.String()); err != nil { + return nil, err + } + for _, set := range credReq.GetAccounts() { + if err := srv.writeConsoleAccountCredentialsMetadataToDB(set.GetAccount(), consolePasswordVersionFld, set.GetVersion()); err != nil { + return nil, err + } + if err := srv.writeConsoleAccountCredentialsMetadataToDB(set.GetAccount(), consolePasswordCreatedOnFld, strconv.FormatUint(set.GetCreatedOn(), 10)); err != nil { + return nil, err + } + } + resp := &credz.RotateAccountCredentialsResponse{ + Response: &credz.RotateAccountCredentialsResponse_Password{}, + } + return resp, nil +} + +func (srv *GNSICredentialzServer) processSshCaPublicKey(req *credz.RotateHostParametersRequest) (*credz.RotateHostParametersResponse, error) { + credReq := req.GetSshCaPublicKey() + if credReq == nil { + return nil, fmt.Errorf(`Unknown request: "%v"`, req) + } + // Sanity checks. + if len(credReq.SshCaPublicKeys) == 0 { + return nil, fmt.Errorf("CA keys cannot be empty") + } + if credReq.GetVersion() == "" { + return nil, fmt.Errorf("version cannot be empty") + } + if credReq.GetCreatedOn() == 0 { + return nil, fmt.Errorf("created_on cannot be empty") + } + for _, key := range credReq.GetSshCaPublicKeys() { + if len(key.GetPublicKey()) == 0 { + return nil, fmt.Errorf("CA public key cannot be empty") + } + } + // Build the message to be sent to the back-end. + var b strings.Builder + fmt.Fprintf(&b, `{ "SshCaPublicKey": [`) + for i, key := range credReq.GetSshCaPublicKeys() { + fmt.Fprintf(&b, ` "%s %s %s"`, sshKeyTypePrefix[key.GetKeyType()], b64.StdEncoding.EncodeToString(key.GetPublicKey()), key.Description) + if i < len(credReq.GetSshCaPublicKeys())-1 { + fmt.Fprintf(&b, `,`) + } + } + fmt.Fprintf(&b, ` ] }`) + sc, err := ssc.NewDbusClientProvider() + if err != nil { + return nil, err + } + defer sc.Close() + if err := sc.SSHMgmtSet(b.String()); err != nil { + return nil, err + } + if err := srv.writeSshHostCredentialsMetadataToDB(sshCaKeysVersionFld, credReq.GetVersion()); err != nil { + return nil, err + } + if err := srv.writeSshHostCredentialsMetadataToDB(sshCaKeysCreatedOnFld, strconv.FormatUint(credReq.GetCreatedOn(), 10)); err != nil { + return nil, err + } + resp := &credz.RotateHostParametersResponse{ + Response: &credz.RotateHostParametersResponse_SshCaPublicKey{}, + } + return resp, nil +} + +// processGlomeConfig processes the GLOME config from the request and +// sends it to the host service to be written to the file system. +func (srv *GNSICredentialzServer) processGlomeConfig(ctx context.Context, req *credz.RotateHostParametersRequest, lastUpdated int64) (*credz.RotateHostParametersResponse, error) { + glomeReq := req.GetGlome() + if glomeReq == nil { + return nil, fmt.Errorf("no glome request found") + } + + // Validate the GLOME request and build the DBUS message. + if err := validateGlomeRequest(glomeReq); err != nil { + return nil, err + } + + // Marshal the GLOME request proto message to JSON. Emits default values such as enabled=false, + // and uses proto field names instead of lowerCamelCase. + jsonBytes, err := protojson.MarshalOptions{ + EmitDefaultValues: true, + UseProtoNames: true, + }.Marshal(glomeReq) + if err != nil { + return nil, fmt.Errorf("failed to marshal Glome request: %v", err) + } + + // Send the DBUS message to SONiC host service. + dbusClient, err := ssc.NewDbusClientProvider() + if err != nil { + return nil, err + } + defer dbusClient.Close() + if err := dbusClient.GLOMEConfigSet(ctx, string(jsonBytes)); err != nil { + return nil, err + } + + // Write the new GLOME config from the request to the STATE_DB. + newGlomeConfigMetadata := &GlomeConfigMetadata{ + Enabled: glomeReq.GetEnabled(), + KeyVersion: glomeReq.GetKeyVersion(), + LastUpdated: lastUpdated, + } + + // Push the new GLOME config to the STATE_DB and update the server data for rollback. + if err := srv.updateGlomeState(ctx, newGlomeConfigMetadata); err != nil { + return nil, err + } + + return &credz.RotateHostParametersResponse{Response: &credz.RotateHostParametersResponse_Glome{}}, nil +} + +// validateGlomeRequest checks if Glome request is valid. +// If GLOME is disabled, ensure other fields are not set. +func validateGlomeRequest(req *credz.GlomeRequest) error { + // Validate GLOME configurations if GLOME is enabled. + if req.GetEnabled() { + if len(req.GetKey()) == 0 { + return fmt.Errorf("GLOME key is empty") + } + if req.GetKeyVersion() <= 0 { + return fmt.Errorf("GLOME key version is not valid") + } + if err := isValidURLPrefix(req.GetUrlPrefix()); err != nil { + return fmt.Errorf("GLOME URL prefix is not valid: %v", err.Error()) + } + return nil + } + // Ensure other fields are not set if GLOME is disabled. + if len(req.GetKey()) > 0 || req.GetKeyVersion() != 0 || len(req.GetUrlPrefix()) != 0 { + return fmt.Errorf("GLOME key, key_version, and url_prefix cannot be set if GLOME is disabled, but received key: %v, key_version: %v, url_prefix: %v", req.GetKey(), req.GetKeyVersion(), req.GetUrlPrefix()) + } + return nil +} + +// isValidURLPrefix checks if the URL prefix is not empty and is a valid URL. +func isValidURLPrefix(prefix string) error { + if len(prefix) == 0 { + return fmt.Errorf("GLOME URL prefix is empty") + } + _, err := url.Parse(prefix) + return err +} + +// SSH Helpers +// writeSshAccountCredentialsMetadataToDB writes the credentials freshness data to the DB. +func (srv *GNSICredentialzServer) writeSshAccountCredentialsMetadataToDB(account, fld, val string) error { + err := writeCredentialsMetadataToDB(sshAccountTbl, account, fld, val) + if err != nil { + return err + } + meta, ok := srv.sshCredMetadata.Accounts[account] + if !ok { + meta = SshAccountVersion{KeysVersion: "unknown", KeysCreatedOn: "0", UsersVersion: "unknown", UsersCreatedOn: "0"} + } + switch fld { + case sshKeysVersionFld: + meta.KeysVersion = val + case sshKeysCreatedOnFld: + meta.KeysCreatedOn = val + case sshPrincipalsVersionFld: + meta.UsersVersion = val + case sshPrincipalsCreatedOnFld: + meta.UsersCreatedOn = val + } + srv.sshCredMetadata.Accounts[account] = meta + return nil +} + +// writeSshHostCredentialsMetadataToDB writes the credentials freshness data to the DB. +func (srv *GNSICredentialzServer) writeSshHostCredentialsMetadataToDB(fld, val string) error { + err := writeCredentialsMetadataToDB(sshHostTbl, "", fld, val) + if err != nil { + return err + } + switch fld { + case sshCaKeysVersionFld: + srv.sshCredMetadata.Host.CaKeysVersion = val + case sshCaKeysCreatedOnFld: + srv.sshCredMetadata.Host.CaKeysCreatedOn = val + } + return nil +} + +// updateGlomeState saves the current GLOME config metadata from STATE_DB to the server for rollback, +// and writes the new GLOME config metadata from GlomeRequest to the STATE_DB. +func (srv *GNSICredentialzServer) updateGlomeState(ctx context.Context, newGlomeConfigMetadata *GlomeConfigMetadata) error { + // Read the current GLOME config from STATE_DB and save it to server data for GLOME rollback. + if currentGlomeConfigMetadata, err := srv.readGlomeConfigMetadataFromStateDB(ctx); err != nil { + log.V(1).Infof("failed to create STATE_DB checkpoint for GLOME: %v", err) + return fmt.Errorf("failed to create STATE_DB checkpoint for GLOME: %v", err) + } else { + srv.glomeConfigMetadata = currentGlomeConfigMetadata + } + + // Write the new GLOME config from the request to the STATE_DB. + if err := srv.writeGlomeConfigMetadataToStateDB(ctx, newGlomeConfigMetadata); err != nil { + return err + } + return nil +} + +type SshAccountVersion struct { + KeysVersion string `json:"keys_version"` + KeysCreatedOn string `json:"keys_created_on"` + UsersVersion string `json:"users_version"` + UsersCreatedOn string `json:"users_created_on"` +} + +type SshHostVersion struct { + CaKeysVersion string `json:"ca_public_keys_version"` + CaKeysCreatedOn string `json:"ca_public_keys_created_on"` +} +type SshCredMetadata struct { + Accounts map[string]SshAccountVersion `json:"accounts"` + Host SshHostVersion `json:"host"` +} + +func NewSshCredMetadata() *SshCredMetadata { + return &SshCredMetadata{ + Accounts: make(map[string]SshAccountVersion), + Host: SshHostVersion{CaKeysVersion: "unknown", CaKeysCreatedOn: "0"}, + } +} + +func (srv *GNSICredentialzServer) checkpointSshCredentialFreshness() { + srv.sshCredMetadataCopy = *srv.sshCredMetadata +} + +func (srv *GNSICredentialzServer) revertSshCredentialFreshness() { + srv.writeSshHostCredentialsMetadataToDB(sshCaKeysVersionFld, srv.sshCredMetadataCopy.Host.CaKeysVersion) + srv.writeSshHostCredentialsMetadataToDB(sshCaKeysCreatedOnFld, srv.sshCredMetadataCopy.Host.CaKeysCreatedOn) + for a, u := range srv.sshCredMetadataCopy.Accounts { + srv.writeSshAccountCredentialsMetadataToDB(a, sshKeysVersionFld, u.KeysVersion) + srv.writeSshAccountCredentialsMetadataToDB(a, sshKeysCreatedOnFld, u.KeysCreatedOn) + srv.writeSshAccountCredentialsMetadataToDB(a, sshPrincipalsVersionFld, u.UsersVersion) + srv.writeSshAccountCredentialsMetadataToDB(a, sshPrincipalsCreatedOnFld, u.UsersCreatedOn) + } +} + +func (srv *GNSICredentialzServer) saveCredentialsFreshness(path string) error { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + if err := enc.Encode(*srv.sshCredMetadata); err != nil { + return err + } + return os.WriteFile(path, buf.Bytes(), 0644) +} + +func (srv *GNSICredentialzServer) loadCredentialFreshness(path string) error { + bytes, err := os.ReadFile(path) + if err != nil { + return err + } + return json.Unmarshal(bytes, srv.sshCredMetadata) +} + +// CONSOLE helpers +// writeConsoleAccountCredentialsMetadataToDB writes the credentials freshness data to the DB. +func (srv *GNSICredentialzServer) writeConsoleAccountCredentialsMetadataToDB(account, fld, val string) error { + if err := writeCredentialsMetadataToDB(consoleAccountTbl, account, fld, val); err != nil { + return err + } + meta, ok := srv.consoleCredMetadata.Accounts[account] + if !ok { + meta = ConsoleAccountVersion{PasswordVersion: "unknown", PasswordCreatedOn: "0"} + } + switch fld { + case consolePasswordVersionFld: + meta.PasswordVersion = val + case consolePasswordCreatedOnFld: + meta.PasswordCreatedOn = val + } + srv.consoleCredMetadata.Accounts[account] = meta + return nil +} + +type ConsoleAccountVersion struct { + PasswordVersion string `json:"password_version"` + PasswordCreatedOn string `json:"password_created_on"` +} + +type ConsoleCredMetadata struct { + Accounts map[string]ConsoleAccountVersion `json:"accounts"` +} + +func NewConsoleCredMetadata() *ConsoleCredMetadata { + return &ConsoleCredMetadata{ + Accounts: make(map[string]ConsoleAccountVersion), + } +} + +func (srv *GNSICredentialzServer) checkpointConsoleFreshness() { + srv.consoleCredMetadataCopy = *srv.consoleCredMetadata +} + +func (srv *GNSICredentialzServer) revertConsoleCredentialFreshness() { + for a, u := range srv.consoleCredMetadataCopy.Accounts { + srv.writeConsoleAccountCredentialsMetadataToDB(a, consolePasswordVersionFld, u.PasswordVersion) + srv.writeConsoleAccountCredentialsMetadataToDB(a, consolePasswordCreatedOnFld, u.PasswordCreatedOn) + } +} + +func (srv *GNSICredentialzServer) saveConsoleCredentialsFreshness(path string) error { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + if err := enc.Encode(*srv.consoleCredMetadata); err != nil { + return err + } + return os.WriteFile(path, buf.Bytes(), 0644) +} + +func (srv *GNSICredentialzServer) loadConsoleCredentialFreshness(path string) error { + bytes, err := os.ReadFile(path) + if err != nil { + return err + } + return json.Unmarshal(bytes, srv.consoleCredMetadata) +} + +// GlomeConfigMetadata is used to store the GLOME config metadata in the STATE_DB and on the server for checkpoint management. +// The default values for the fields are: +// - Enabled: false (bool) +// - KeyVersion: 0 (int32) +// - LastUpdated: 0 (int64) +type GlomeConfigMetadata struct { + Enabled bool `json:"enabled"` + KeyVersion int32 `json:"key_version"` + LastUpdated int64 `json:"last_updated"` // Time in Unix epoch nanoseconds in string format. +} + +var defaultGlomeConfigMetadata = &GlomeConfigMetadata{Enabled: false, KeyVersion: 0, LastUpdated: 0} + +// InitGlomeConfigMetadata sets the server's initial glomeConfigMetadata from the data received from STATE_DB. +// - If the GlomeConfigMetadata does not exist in the STATE_DB, it sets the initial value to the default GlomeConfigMetadata. +// - If any error occurs, it sets the initial value to nil. +func (srv *GNSICredentialzServer) InitGlomeConfigMetadata(ctx context.Context) { + // Get the GlomeConfigMetadata from the STATE_DB. + glomeConfigMetadata, err := srv.readGlomeConfigMetadataFromStateDB(ctx) + if err != nil { + log.V(0).Infof("failed to read GLOME config metadata from STATE_DB: %v", err) + srv.glomeConfigMetadata = nil + return + } + + // If the Glome config metadata does not exist in the STATE_DB, write the default values + // to the STATE_DB and return the default GlomeConfigMetadata. + if glomeConfigMetadata == defaultGlomeConfigMetadata { + if err := srv.writeGlomeConfigMetadataToStateDB(ctx, defaultGlomeConfigMetadata); err != nil { + // Even with error, we still return the default values here because the STATE_DB empty is + // the same as the STATE_DB with default values. + log.V(0).Infof("failed to write default GLOME config metadata to STATE_DB: %v", err) + } + srv.glomeConfigMetadata = defaultGlomeConfigMetadata + return + } + + // If the glome config metadata exists in the STATE_DB, write it to the server. + srv.glomeConfigMetadata = glomeConfigMetadata +} + +func (srv *GNSICredentialzServer) readGlomeConfigMetadataFromStateDB(ctx context.Context) (*GlomeConfigMetadata, error) { + // Read the GLOME config metadata from the STATE_DB. + glomeKey := common_utils.GetKey([]string{credentialsTbl, glomeConfigRedisKey}) + result, err := srv.stateDbClient.HGetAll(context.Background(), glomeKey).Result() + if err != nil { + return nil, fmt.Errorf("failed to get GLOME config metadata from STATE_DB: %v", err) + } + + // If the GLOME config metadata does not exist in the STATE_DB, return default values + // for GlomeConfigMetadata. + if len(result) == 0 { + return defaultGlomeConfigMetadata, nil + } + + enabled, err := strconv.ParseBool(result["enabled"]) + if err != nil { + return nil, fmt.Errorf("failed to parse 'enabled' field in GLOME config metadata from STATE_DB: %v", err) + } + keyVersion, err := strconv.ParseInt(result["key_version"], 10, 32) + if err != nil { + return nil, fmt.Errorf("failed to parse 'key_version' field in GLOME config metadata from STATE_DB: %v", err) + } + lastUpdated, err := strconv.ParseInt(result["last_updated"], 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse 'last_updated' field in GLOME config metadata from STATE_DB: %v", err) + } + + return &GlomeConfigMetadata{ + Enabled: enabled, + KeyVersion: int32(keyVersion), + LastUpdated: int64(lastUpdated), + }, nil +} + +func (srv *GNSICredentialzServer) writeGlomeConfigMetadataToStateDB(ctx context.Context, newGlomeConfigMetadata *GlomeConfigMetadata) error { + if newGlomeConfigMetadata == nil { + return fmt.Errorf("newGlomeConfigMetadata is nil") + } + + // Write the GLOME config metadata to the STATE_DB. + glomeKey := common_utils.GetKey([]string{credentialsTbl, glomeConfigRedisKey}) + glomeFields := map[string]interface{}{ + "enabled": newGlomeConfigMetadata.Enabled, + "key_version": newGlomeConfigMetadata.KeyVersion, + "last_updated": newGlomeConfigMetadata.LastUpdated, + } + err := srv.stateDbClient.HSet(ctx, glomeKey, glomeFields).Err() + if err != nil { + return fmt.Errorf("failed to write GLOME config metdata to STATE_DB: %v", err) + } + + return nil +} diff --git a/gnmi_server/gnsi_credentialz_test.go b/gnmi_server/gnsi_credentialz_test.go new file mode 100644 index 000000000..0af834c2d --- /dev/null +++ b/gnmi_server/gnsi_credentialz_test.go @@ -0,0 +1,2955 @@ +package gnmi + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "os" + "reflect" + "strings" + "sync" + "testing" + "time" + + "github.com/go-redis/redismock/v9" + log "github.com/golang/glog" + "github.com/google/go-cmp/cmp" + credz "github.com/openconfig/gnsi/credentialz" + ssc "github.com/sonic-net/sonic-gnmi/sonic_service_client" + testcert "github.com/sonic-net/sonic-gnmi/testdata/tls" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/keepalive" + "google.golang.org/grpc/status" +) + +const ( + sshMetaPathTest = "../testdata/gnsi/ssh-version.json" + consoleMetaPathTest = "../testdata/gnsi/console-version.json" + stateDbKeyForGlome = "CREDENTIALS|GLOME_CONFIG" + + expectedSshCreateCmd = "ssh_mgmt" + string(ssc.CredzCPCreate) + expectedSshDeleteCmd = "ssh_mgmt" + string(ssc.CredzCPDelete) + expectedSshRestoreCmd = "ssh_mgmt" + string(ssc.CredzCPRestore) + expectedSshSetCmd = "ssh_mgmt.set" + expectedConsoleCreateCmd = "gnsi_console" + string(ssc.CredzCPCreate) + expectedConsoleDeleteCmd = "gnsi_console" + string(ssc.CredzCPDelete) + expectedConsoleRestoreCmd = "gnsi_console" + string(ssc.CredzCPRestore) + expectedConsoleSetCmd = "gnsi_console.set" + expectedGlomePushConfigCmd = "glome" + string(ssc.CredzGlomePushConfig) // Used for both create checkpoint and set new glome config. + expectedGlomeConfigRestoreCmd = "glome" + string(ssc.CredzCPRestore) + + expectedValidGlomeRequestInJson = `{"enabled":true,"key":"test-key","key_version":1,"url_prefix":"https://test.com/"}` + expectedDisableGlomeRequestInJson = `{"enabled":false,"key":"","key_version":0,"url_prefix":""}` +) + +var ( + // glomePushConfigDbusMessageForValidRequest is the expected D-Bus message for pushing a valid GLOME config. + glomePushConfigDbusMessageForValidRequest = mockDBusMessage{ + methodName: expectedGlomePushConfigCmd, + cmd: expectedValidGlomeRequestInJson, + } + + // glomePushConfigDbusMessageForDisableRequest is the expected D-Bus message for pushing a config for disabling GLOME. + glomePushConfigDbusMessageForDisableRequest = mockDBusMessage{ + methodName: expectedGlomePushConfigCmd, + cmd: expectedDisableGlomeRequestInJson, + } + + // glomeRestoreDbusMessage is the expected D-Bus message for restoring the GLOME config to the + // checkpoint state. + glomeRestoreDbusMessage = mockDBusMessage{ + methodName: expectedGlomeConfigRestoreCmd, + } + + // validRotateHostParametersGlomeRequest is a valid RotateHostParametersRequest for GLOME. + validRotateHostParametersGlomeRequest = &credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Glome{ + Glome: &credz.GlomeRequest{ + Enabled: true, + Key: "test-key", + KeyVersion: 1, + UrlPrefix: "https://test.com/", + }, + }, + } + + // rotateHostParametersDisableGlomeRequest is a RotateHostParametersRequest for disabling GLOME. + rotateHostParametersDisableGlomeRequest = &credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Glome{ + Glome: &credz.GlomeRequest{ + Enabled: false, + }, + }, + } +) + +func createCredzServer(t *testing.T, cfg *Config) *Server { + t.Helper() + certificate, err := testcert.NewCert() + if err != nil { + t.Fatalf("could not load server key pair: %s", err) + } + tlsCfg := &tls.Config{ + ClientAuth: tls.RequestClientCert, + Certificates: []tls.Certificate{certificate}, + } + + tlsOpts := []grpc.ServerOption{grpc.Creds(credentials.NewTLS(tlsCfg))} + keep_alive_params := keepalive.ServerParameters{ + MaxConnectionIdle: 1 * time.Second, + } + commonOpts := []grpc.ServerOption{ + grpc.KeepaliveParams(keep_alive_params), + } + s, err := NewServer(cfg, tlsOpts, commonOpts) + if err != nil { + t.Fatalf("Failed to create gNMI server: %v", err) + } + return s +} + +var sshTests = []struct { + desc string + f func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) +}{ + { + desc: "Unimplemented ServerKeys", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateHostParameters(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_ServerKeys{}}); err != nil { + t.Fatal(err) + } + // Check that the client receives an Unimplemented error. + if resp, err := c.Recv(); status.Code(err) != codes.Unimplemented || resp != nil { + t.Fatalf("expected Unimplemented error ; got resp: %v, err: %v", resp, err) + } + }, + }, + { + desc: "Unimplemented GenerateKeys", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateHostParameters(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_GenerateKeys{}}); err != nil { + t.Fatal(err) + } + // Check that the client receives an Unimplemented error. + if resp, err := c.Recv(); status.Code(err) != codes.Unimplemented || resp != nil { + t.Fatalf("expected Unimplemented error ; got resp: %v, err: %v", resp, err) + } + }, + }, + { + desc: "Unimplemented AuthenticationAllowed", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateHostParameters(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_AuthenticationAllowed{}}); err != nil { + t.Fatal(err) + } + // Check that the client receives an Unimplemented error. + if resp, err := c.Recv(); status.Code(err) != codes.Unimplemented || resp != nil { + t.Fatalf("expected Unimplemented error ; got resp: %v, err: %v", resp, err) + } + }, + }, + { + desc: "Unimplemented AuthorizedPrincipalCheck", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateHostParameters(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_AuthorizedPrincipalCheck{}}); err != nil { + t.Fatal(err) + } + // Check that the client receives an Unimplemented error. + if resp, err := c.Recv(); status.Code(err) != codes.Unimplemented || resp != nil { + t.Fatalf("expected Unimplemented error ; got resp: %v, err: %v", resp, err) + } + }, + }, + { + desc: "User scenario: keys, users, finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{ + Credential: &credz.AuthorizedKeysRequest{ + Credentials: []*credz.AccountCredentials{ + { + Account: "root", + Version: "root-version-1", + CreatedOn: 123, + AuthorizedKeys: []*credz.AccountCredentials_AuthorizedKey{ + { + AuthorizedKey: []byte("Authorized-key #1"), + Options: []*credz.Option{ + { + Key: &credz.Option_Name{Name: "from"}, + Value: "*.sales.example.net,!pc.sales.example.net", + }, + }, + }, + { + AuthorizedKey: []byte("Authorized-key #2"), + }, + }, + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + resp, err := c.Recv() + if err != nil { + t.Fatal(err) + } + if resp.GetResponse() == nil { + t.Fatal("expected a message") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedSshCreateCmd, dbus) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshSetCmd, `{ "SshAccountKeys": [ { "account": "root", "keys": [ { "key" : "unspecified QXV0aG9yaXplZC1rZXkgIzE= ", "options" : [ { "name" : "from", "value": "*.sales.example.net,!pc.sales.example.net" } ] }, { "key" : "unspecified QXV0aG9yaXplZC1rZXkgIzI= ", "options" : [ ] } ] } ] }`}) { + t.Fatalf("DBUS Failure wanted ssh_mgmt.set; got: %v", dbus) + } + + err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{ + User: &credz.AuthorizedUsersRequest{ + Policies: []*credz.UserPolicy{ + { + Account: "root", + Version: "root-version-2", + CreatedOn: 123, + AuthorizedPrincipals: &credz.UserPolicy_SshAuthorizedPrincipals{ + AuthorizedPrincipals: []*credz.UserPolicy_SshAuthorizedPrincipal{ + &credz.UserPolicy_SshAuthorizedPrincipal{ + AuthorizedUser: "alice", + Options: []*credz.Option{ + { + Key: &credz.Option_Name{Name: "from"}, + Value: "*.sales.example.net,!pc.sales.example.net", + }, + }, + }, + &credz.UserPolicy_SshAuthorizedPrincipal{ + AuthorizedUser: "bob", + }, + }, + }, + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + resp, err = c.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshSetCmd, `{ "SshAccountUsers": [ { "account": "root", "users": [ { "name" : "alice", "options" : [ { "name" : "from", "value": "*.sales.example.net,!pc.sales.example.net" } ] }, { "name" : "bob", "options" : [ ] } ] } ] }`}) { + t.Fatalf("DBUS Failure wanted ssh_mgmt.create_checkpoint; got: %v", dbus) + } + + if err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Finalize{}, + }); err != nil { + t.Fatal(err.Error()) + } + if resp, err := c.Recv(); err != io.EOF || resp.GetResponse() != nil { + t.Fatalf("expected EOF; err: %v; resp: %v", err, resp) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshDeleteCmd, ""}) { + t.Fatalf("DBUS Failure wanted %v; got: %v", expectedSshDeleteCmd, dbus) + } + }, + }, + { + desc: "User scenario: keys, finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{ + Credential: &credz.AuthorizedKeysRequest{ + Credentials: []*credz.AccountCredentials{ + { + Account: "root", + Version: "root-version-1", + CreatedOn: 123, + AuthorizedKeys: []*credz.AccountCredentials_AuthorizedKey{ + { + AuthorizedKey: []byte("Authorized-key #1"), + }, + { + AuthorizedKey: []byte("Authorized-key #2"), + }, + }, + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + resp, err := c.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedSshCreateCmd, dbus) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshSetCmd, `{ "SshAccountKeys": [ { "account": "root", "keys": [ { "key" : "unspecified QXV0aG9yaXplZC1rZXkgIzE= ", "options" : [ ] }, { "key" : "unspecified QXV0aG9yaXplZC1rZXkgIzI= ", "options" : [ ] } ] } ] }`}) { + t.Fatalf("DBUS Failure wanted ssh_mgmt.set; got: %v", dbus) + } + if err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Finalize{}, + }); err != nil { + t.Fatal(err.Error()) + } + if resp, err := c.Recv(); err != io.EOF || resp.GetResponse() != nil { + t.Fatalf("expected EOF; err: %v; resp: %v", err, resp) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshDeleteCmd, ""}) { + t.Fatalf("DBUS Failure wanted %v; got: %v", expectedSshDeleteCmd, dbus) + } + }, + }, + { + desc: "User scenario: users, finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{ + User: &credz.AuthorizedUsersRequest{ + Policies: []*credz.UserPolicy{ + { + Account: "root", + Version: "2021-09-10T18:22:46", + CreatedOn: 1631298166, + AuthorizedPrincipals: &credz.UserPolicy_SshAuthorizedPrincipals{ + AuthorizedPrincipals: []*credz.UserPolicy_SshAuthorizedPrincipal{ + &credz.UserPolicy_SshAuthorizedPrincipal{ + AuthorizedUser: "alice", + }, + &credz.UserPolicy_SshAuthorizedPrincipal{ + AuthorizedUser: "bob", + }, + }, + }, + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + resp, err := c.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedSshCreateCmd, dbus) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshSetCmd, `{ "SshAccountUsers": [ { "account": "root", "users": [ { "name" : "alice", "options" : [ ] }, { "name" : "bob", "options" : [ ] } ] } ] }`}) { + t.Fatalf("DBUS Failure wanted ssh_mgmt.set; got: %v", dbus) + } + if err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Finalize{}, + }); err != nil { + t.Fatal(err.Error()) + } + if resp, err := c.Recv(); err != io.EOF || resp.GetResponse() != nil { + t.Fatalf("expected EOF; err: %v; resp: %v", err, resp) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshDeleteCmd, ""}) { + t.Fatalf("DBUS Failure wanted %v; got: %v", expectedSshDeleteCmd, dbus) + } + }, + }, + { + desc: "User scenario: keys, users, no finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{ + Credential: &credz.AuthorizedKeysRequest{ + Credentials: []*credz.AccountCredentials{ + { + Account: "root", + Version: "root-version-1", + CreatedOn: 123, + AuthorizedKeys: []*credz.AccountCredentials_AuthorizedKey{ + { + AuthorizedKey: []byte("Authorized-key #1"), + }, + { + AuthorizedKey: []byte("Authorized-key #2"), + }, + }, + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + resp, err := c.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedSshCreateCmd, dbus) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshSetCmd, `{ "SshAccountKeys": [ { "account": "root", "keys": [ { "key" : "unspecified QXV0aG9yaXplZC1rZXkgIzE= ", "options" : [ ] }, { "key" : "unspecified QXV0aG9yaXplZC1rZXkgIzI= ", "options" : [ ] } ] } ] }`}) { + t.Fatalf("DBUS Failure wanted ssh_mgmt.set; got: %v", dbus) + } + err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{ + User: &credz.AuthorizedUsersRequest{ + Policies: []*credz.UserPolicy{ + { + Account: "root", + Version: "root-version-2", + CreatedOn: 123, + AuthorizedPrincipals: &credz.UserPolicy_SshAuthorizedPrincipals{ + AuthorizedPrincipals: []*credz.UserPolicy_SshAuthorizedPrincipal{ + &credz.UserPolicy_SshAuthorizedPrincipal{ + AuthorizedUser: "alice", + }, + &credz.UserPolicy_SshAuthorizedPrincipal{ + AuthorizedUser: "bob", + }, + }, + }, + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + resp, err = c.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshSetCmd, `{ "SshAccountUsers": [ { "account": "root", "users": [ { "name" : "alice", "options" : [ ] }, { "name" : "bob", "options" : [ ] } ] } ] }`}) { + t.Fatalf("DBUS Failure wanted ssh_mgmt.set; got: %v", dbus) + } + if err = c.CloseSend(); err != nil { + t.Fatal(err.Error()) + } + resp, err = c.Recv() + if 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) + } + if resp != nil { + t.Fatal("Received unexpected message after closing connection.") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshRestoreCmd, ""}) { + t.Fatalf("DBUS Failure wanted %v; got: %v", expectedSshRestoreCmd, dbus) + } + }, + }, + { + desc: "User scenario: keys, no finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{ + Credential: &credz.AuthorizedKeysRequest{ + Credentials: []*credz.AccountCredentials{ + { + Account: "root", + Version: "root-version-1", + CreatedOn: 123, + AuthorizedKeys: []*credz.AccountCredentials_AuthorizedKey{ + { + AuthorizedKey: []byte("Authorized-key #1"), + }, + { + AuthorizedKey: []byte("Authorized-key #2"), + }, + }, + }, + }, + }, + }, + }) + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedSshCreateCmd, dbus) + } + if err != nil { + t.Fatal(err.Error()) + } + resp, err := c.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshSetCmd, `{ "SshAccountKeys": [ { "account": "root", "keys": [ { "key" : "unspecified QXV0aG9yaXplZC1rZXkgIzE= ", "options" : [ ] }, { "key" : "unspecified QXV0aG9yaXplZC1rZXkgIzI= ", "options" : [ ] } ] } ] }`}) { + t.Fatalf("DBUS Failure wanted ssh_mgmt.set; got: %v", dbus) + } + if err = c.CloseSend(); err != nil { + t.Fatal(err.Error()) + } + resp, err = c.Recv() + if 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) + } + if resp != nil { + t.Fatal("Received unexpected message after closing connection.") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshRestoreCmd, ""}) { + t.Fatalf("DBUS Failure wanted %v; got: %v", expectedSshRestoreCmd, dbus) + } + }, + }, + { + desc: "User scenario: users, no finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{ + User: &credz.AuthorizedUsersRequest{ + Policies: []*credz.UserPolicy{ + { + Account: "root", + Version: "root-version-2", + CreatedOn: 123, + AuthorizedPrincipals: &credz.UserPolicy_SshAuthorizedPrincipals{ + AuthorizedPrincipals: []*credz.UserPolicy_SshAuthorizedPrincipal{ + &credz.UserPolicy_SshAuthorizedPrincipal{ + AuthorizedUser: "alice", + }, + &credz.UserPolicy_SshAuthorizedPrincipal{ + AuthorizedUser: "bob", + }, + }, + }, + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + resp, err := c.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedSshCreateCmd, dbus) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshSetCmd, `{ "SshAccountUsers": [ { "account": "root", "users": [ { "name" : "alice", "options" : [ ] }, { "name" : "bob", "options" : [ ] } ] } ] }`}) { + t.Fatalf("DBUS Failure wanted ssh_mgmt.set; got: %v", dbus) + } + if err = c.CloseSend(); err != nil { + t.Fatal(err.Error()) + } + resp, err = c.Recv() + if 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) + } + if resp != nil { + t.Fatal("Received unexpected message after closing connection.") + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedSshRestoreCmd, ""}) { + t.Fatalf("DBUS Failure wanted %v; got: %v", expectedSshRestoreCmd, dbus) + } + }, + }, + { + desc: "User scenario: graceful close", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.CloseSend(); err != nil { + t.Fatal(err.Error()) + } + resp, err := c.Recv() + if 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) + } + if resp != nil { + t.Fatal("Received unexpected message after closing connection.") + } + + select { + case msg := <-ch: + t.Fatalf("Unexpected DBUS msg %v", msg) + default: + } + }, + }, + { + desc: "Host scenario: ca_public_key, finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + var err error + var h credz.Credentialz_RotateHostParametersClient + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + h, err = sc.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("ModifySshCaPublicKey", ch, t, + []mockDBusMessage{ + { + methodName: expectedSshCreateCmd, + cmd: "", + }, + { + methodName: expectedSshSetCmd, + cmd: `{ "SshCaPublicKey": [ "ssh-ed25519 VEVTVC1DRVJUICMx test#1", "ssh-rsa VEVTVC1DRVJUICMy test#2" ] }`, + }, + }, + func() error { + err = h.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{ + SshCaPublicKey: &credz.CaPublicKeyRequest{ + Version: "CA-trust-bundle-1", + CreatedOn: 123, + SshCaPublicKeys: []*credz.PublicKey{ + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #1"), + KeyType: credz.KeyType_KEY_TYPE_ED25519, + Description: "test#1", + }, + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #2"), + KeyType: credz.KeyType_KEY_TYPE_RSA_2048, + Description: "test#2", + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + resp, err := h.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + return nil + }) + runRotateHostParameters("ModifyFinalize", ch, t, []mockDBusMessage{{methodName: expectedSshDeleteCmd, cmd: ""}}, func() error { + err = h.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Finalize{}, + }) + if err != nil { + t.Fatal(err.Error()) + } + if resp, err := h.Recv(); err != io.EOF || resp.GetResponse() != nil { + t.Fatalf("expected EOF; err: %v; resp: %v", err, resp) + } + return nil + }) + }, + }, + { + desc: "Host scenario: ca_public_key, no finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + var err error + var h credz.Credentialz_RotateHostParametersClient + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + h, err = sc.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("ModifySshCaPublicKey", ch, t, + []mockDBusMessage{ + { + methodName: expectedSshCreateCmd, + cmd: "", + }, + { + methodName: expectedSshSetCmd, + cmd: `{ "SshCaPublicKey": [ "ssh-ed25519 VEVTVC1DRVJUICMx test#1", "ssh-rsa VEVTVC1DRVJUICMy test#2" ] }`, + }, + }, + func() error { + err = h.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{ + SshCaPublicKey: &credz.CaPublicKeyRequest{ + Version: "CA-trust-bundle-1", + CreatedOn: 123, + SshCaPublicKeys: []*credz.PublicKey{ + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #1"), + KeyType: credz.KeyType_KEY_TYPE_ED25519, + Description: "test#1", + }, + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #2"), + KeyType: credz.KeyType_KEY_TYPE_RSA_2048, + Description: "test#2", + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + resp, err := h.Recv() + if err != nil { + t.Fatal(err.Error()) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + return nil + }) + runRotateHostParameters("CloseStream", ch, t, []mockDBusMessage{{methodName: expectedSshRestoreCmd, cmd: ""}}, func() error { + if err = h.CloseSend(); err != nil { + t.Fatal(err.Error()) + } + resp, err := h.Recv() + if 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) + } + if resp != nil { + t.Fatal("Received unexpected message after closing connection.") + } + return nil + }) + }, + }, + { + desc: "Host scenario: graceful close", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + var err error + var h credz.Credentialz_RotateHostParametersClient + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + h, err = sc.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("CloseStream", ch, t, []mockDBusMessage{}, func() error { + if err = h.CloseSend(); err != nil { + t.Fatal(err.Error()) + } + resp, err := h.Recv() + if status.Code(err) != codes.Aborted { + t.Fatalf("Unexpected error: %v", err) + } + if resp != nil { + t.Fatal("Received unexpected message after closing connection.") + } + return nil + }) + }, + }, + { + desc: "read JSON with version info, fail", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + s := &GNSICredentialzServer{ + sshCredMetadata: NewSshCredMetadata(), + } + if err := s.loadCredentialFreshness(""); err == nil { + t.Fatal("Expected file read error") + } + }, + }, + { + desc: "read JSON with version info, success", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + s := &GNSICredentialzServer{ + sshCredMetadata: NewSshCredMetadata(), + } + if err := s.loadCredentialFreshness(sshMetaPathTest); err != nil { + t.Fatal(err) + } + }, + }, + { + desc: "write JSON with version info, fail", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + s := &GNSICredentialzServer{ + sshCredMetadata: NewSshCredMetadata(), + } + if err := s.saveCredentialsFreshness(""); err == nil { + t.Fatal("Expected write file error") + } + }, + }, + { + desc: "write JSON with version info, success", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + s := &GNSICredentialzServer{ + sshCredMetadata: NewSshCredMetadata(), + } + if err := s.saveCredentialsFreshness(sshMetaPathTest); err != nil { + t.Fatal(err) + } + }, + }, + { + desc: "Reject concurrent account", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateAccountCredentialsRequest{}); err != nil { + t.Fatal(err) + } + c2, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + if err = c2.Send(&credz.RotateAccountCredentialsRequest{}); err != nil { + t.Fatal(err) + } + if _, err := c2.Recv(); status.Code(err) != codes.Aborted { + t.Errorf("expected: Aborted, got: %+v", err) + } + }, + }, + { + desc: "Reject concurrent host", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateHostParameters(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateHostParametersRequest{}); err != nil { + t.Fatal(err) + } + c2, err := sc.RotateHostParameters(ctx) + if err != nil { + t.Fatal(err) + } + if err = c2.Send(&credz.RotateHostParametersRequest{}); err != nil { + t.Fatal(err) + } + if _, err := c2.Recv(); status.Code(err) != codes.Aborted { + t.Errorf("expected: Aborted, got: %+v", err) + } + }, + }, + { + desc: "Host scenario: ca_public_key, ca_public_key", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + var err error + var h credz.Credentialz_RotateHostParametersClient + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + h, err = sc.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("ModifySshCaPublicKey", ch, t, + []mockDBusMessage{ + { + methodName: expectedSshCreateCmd, + cmd: "", + }, + { + methodName: expectedSshSetCmd, + cmd: `{ "SshCaPublicKey": [ "ssh-ed25519 VEVTVC1DRVJUICMx test#1", "ssh-rsa VEVTVC1DRVJUICMy test#2" ] }`, + }, + }, + func() error { + if err = h.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{ + SshCaPublicKey: &credz.CaPublicKeyRequest{ + Version: "CA-trust-bundle-1", + CreatedOn: 123, + SshCaPublicKeys: []*credz.PublicKey{ + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #1"), + KeyType: credz.KeyType_KEY_TYPE_ED25519, + Description: "test#1", + }, + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #2"), + KeyType: credz.KeyType_KEY_TYPE_RSA_2048, + Description: "test#2", + }, + }, + }, + }, + }); err != nil { + t.Fatalf("h.Send() failed: %v", err) + } + resp, err := h.Recv() + if err != nil { + t.Fatalf("h.Recv() failed: %v", err) + } + if resp.GetResponse() == nil { + t.Fatal("resp.GetResponse() returned nil; expected a message") + } + return nil + }) + runRotateHostParameters("SecondModifySshCaPublicKey", ch, t, []mockDBusMessage{{methodName: expectedSshRestoreCmd, cmd: ""}}, + func() error { + if err = h.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{ + SshCaPublicKey: &credz.CaPublicKeyRequest{ + Version: "CA-trust-bundle-2", + CreatedOn: 123, + SshCaPublicKeys: []*credz.PublicKey{ + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #3"), + KeyType: credz.KeyType_KEY_TYPE_ED25519, + Description: "test#3", + }, + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #4"), + KeyType: credz.KeyType_KEY_TYPE_RSA_2048, + Description: "test#4", + }, + }, + }, + }, + }); err != nil { + t.Fatalf("h.Send() failed: %v", err) + } + // Check that the client receives an Aborted error due to multiple SSH CA public key requests. + if resp, err := h.Recv(); status.Code(err) != codes.Aborted || resp != nil { + t.Errorf("h.Recv() returned resp: %v, err: %v; expected error status code: %v", resp, err, codes.Aborted) + } + return nil + }) + }, + }, + { + desc: "Host scenario: ca_public_key, glome", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + var err error + var h credz.Credentialz_RotateHostParametersClient + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + h, err = sc.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("ModifySshCaPublicKey", ch, t, + []mockDBusMessage{ + { + methodName: expectedSshCreateCmd, + cmd: "", + }, + { + methodName: expectedSshSetCmd, + cmd: `{ "SshCaPublicKey": [ "ssh-ed25519 VEVTVC1DRVJUICMx test#1", "ssh-rsa VEVTVC1DRVJUICMy test#2" ] }`, + }, + }, + func() error { + if err = h.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{ + SshCaPublicKey: &credz.CaPublicKeyRequest{ + Version: "CA-trust-bundle-1", + CreatedOn: 123, + SshCaPublicKeys: []*credz.PublicKey{ + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #1"), + KeyType: credz.KeyType_KEY_TYPE_ED25519, + Description: "test#1", + }, + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #2"), + KeyType: credz.KeyType_KEY_TYPE_RSA_2048, + Description: "test#2", + }, + }, + }, + }, + }); err != nil { + t.Fatalf("h.Send() failed: %v", err) + } + resp, err := h.Recv() + if err != nil { + t.Fatalf("h.Recv() failed: %v", err) + } + if resp.GetResponse() == nil { + t.Fatal("resp.GetResponse() returned nil; expected a message") + } + return nil + }) + runRotateHostParameters("GlomeRequest", ch, t, []mockDBusMessage{{methodName: expectedSshRestoreCmd, cmd: ""}}, + func() error { + if err = h.Send(validRotateHostParametersGlomeRequest); err != nil { + t.Fatalf("h.Send() failed: %v", err) + } + // Check that the client receives an Aborted error due to Glome request after SSH CA public key request. + if resp, err := h.Recv(); status.Code(err) != codes.Aborted || resp != nil { + t.Errorf("h.Recv() returned resp: %v, err: %v; expected error status code: %v", resp, err, codes.Aborted) + } + return nil + }) + }, + }, +} + +// TestSSHServer tests implementation of gnsi.Ssh server. +func TestGnsiCredzSSHServer(t *testing.T) { + t.Helper() + cfg := &Config{SshCredMetaFile: sshMetaPathTest, ConsoleCredMetaFile: consoleMetaPathTest, Port: 8081} + s := createCredzServer(t, cfg) + go runServer(t, s) + defer s.Stop() + + metaBackup, err := os.ReadFile(sshMetaPathTest) + if err != nil { + t.Fatal(err) + } + defer os.WriteFile(sshMetaPathTest, metaBackup, 0600) + + var dbusListener chan []string + var done chan bool + var resetFunc func() + + dbusListener, done, resetFunc = newMockSshDbusServer() + defer resetFunc() + defer func() { done <- true }() // Signal the mock goroutine to exit + + // Create a gNSI.ssh client and connect it to the gNSI.ssh server. + tlsConfig := &tls.Config{InsecureSkipVerify: true} + opts := []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))} + targetAddr := fmt.Sprintf("127.0.0.1:%d", s.config.Port) + var credzMu sync.Mutex + for _, tc := range sshTests { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + t.Run(tc.desc, func(t *testing.T) { + credzMu.Lock() + defer credzMu.Unlock() + conn, err := grpc.Dial(targetAddr, opts...) + if err != nil { + t.Fatalf("Dialing to %s failed: %v", targetAddr, err) + } + defer conn.Close() + credzClient := credz.NewCredentialzClient(conn) + tc.f(t, ctx, credzClient, dbusListener) + }) + cancel() + } + done <- true + // Save the SSH Credentials metadata to a file. + if err := s.gnsiCredentialz.saveCredentialsFreshness(s.config.SshCredMetaFile); err != nil { + t.Fatal(err) + } +} + +func newMockSshDbusServer() (chan []string, chan bool, func()) { + // dbusServerInputChan: This is the channel that the FakeClient will write to. + dbusServerInputChan := make(chan []string, 10) + // listenerChan: This is the channel that the test function (via dbusListen) will read from. + listenerChan := make(chan []string, 10) + done := make(chan bool, 1) + + // Initialize our FakeClient, directing its output to dbusServerInputChan. + fakeService := &ssc.FakeClient{Command: dbusServerInputChan} + + // 1. Save the original provider + originalProvider := ssc.NewDbusClientProvider + + // 2. Overwrite the provider with our mock + ssc.NewDbusClientProvider = func() (ssc.Service, error) { + return fakeService, nil + } + + // 3. Define the cleanup/reset function + resetFunc := func() { + ssc.NewDbusClientProvider = originalProvider + } + go func() { + checkpoint := false + var mu sync.Mutex + for { + select { + case <-done: // if cancel() execute + log.V(2).Infoln("Shutting down the DBUS server.") + return + case msg := <-dbusServerInputChan: + mu.Lock() + log.V(2).Infof("DBUS service received %+v", msg) + switch msg[0] { + case expectedSshCreateCmd: + if checkpoint { + log.Fatal("mock ssh_mgmt service: checkpoint already exists.") + } + checkpoint = true + case expectedSshDeleteCmd: + if !checkpoint { + log.Fatal("mock ssh_mgmt service: checkpoint does not exists.") + } + checkpoint = false + case expectedSshRestoreCmd: + if !checkpoint { + log.Fatal("mock ssh_mgmt service: checkpoint does not exists.") + } + checkpoint = false + case expectedSshSetCmd: + if !checkpoint { + log.Fatal("mock ssh_mgmt service: set without checkpoint.") + } + default: + log.V(2).Infof("mock ssh_mgmt service: unknown service: %+v", msg) + } + listenerChan <- msg + mu.Unlock() + } + } + }() + + return listenerChan, done, resetFunc +} + +func dbusListen(dbus <-chan []string) []string { + select { + case resp := <-dbus: + return resp + case <-time.After(time.Second * 3): + return []string{"dbus listener timed out"} + } +} + +func run(name string, dbus <-chan []string, t *testing.T, cmd []string, f func() error) { + t.Run(name, func(t *testing.T) { + var wg sync.WaitGroup + wg.Add(1) + finished := make(chan error, 1) + go func() { + finished <- f() + wg.Done() + }() + count := 2 + if len(cmd) == 0 { + count = 1 + } + for i := 0; i < count; i++ { + select { + case resp := <-dbus: + log.V(2).Infof("received on DBUS: %v\n", resp) + for i := range cmd { + if cmd[i] != resp[i] { + t.Errorf("expected: '%v' but got '%v'", cmd, resp) + } + } + if len(cmd) == 2 && !json.Valid([]byte(cmd[1])) { + t.Errorf("malformed JSON string: '%v'", cmd[1]) + } + case err := <-finished: + log.V(2).Infof("f() is done with err=%v\n", err) + if err != nil { + t.Error(err.Error()) + } + case <-time.After(time.Second * 3): + t.Errorf("did not get expected DBUS message and/or %v() did not finish within 5s", name) + } + } + wg.Wait() + log.V(2).Infoln("Finished:", name) + }) +} + +// runRotateHostParameters runs the clientAction() and waits for the DBUS messages. +// The clientAction() is expected to send a request to the server and wait for the response. +// The DBUS messages are expected to be sent by the server to the host service. +func runRotateHostParameters(name string, dbusListener <-chan []string, t *testing.T, expectedDbusMsgs []mockDBusMessage, clientAction func() error) { + testName := "RotateHostParameters_" + name + t.Run(testName, func(t *testing.T) { + // finished is used to wait for the clientAction() to finish. + finished := make(chan error, 1) + go func() { + finished <- clientAction() + }() + + // Wait for the DBus messages and check if they are the same as expected. + for _, expectedDbusMsg := range expectedDbusMsgs { + select { + case resp := <-dbusListener: + log.V(2).Infof("received on DBUS: %v\n", resp) + if expectedDbusMsg.methodName != resp[0] { + t.Errorf("expected DBUS message's method name: '%v' but got '%v'", expectedDbusMsg.methodName, resp[0]) + continue + } + // If restore checkpoint methodName, nothing should be sent for the command. + if expectedDbusMsg.methodName == expectedGlomeConfigRestoreCmd { + continue + } + // 3. Handle Set/Push commands (which have JSON payload) + if len(resp) < 2 { + t.Errorf("expected JSON payload for method %v, but got none", resp[0]) + continue + } + // Check if the received DBUS message's cmd is the same as expected. + // A direct string comparison is a fast path. If they differ, and a non-empty + // cmd is expected, fall back to a more lenient JSON comparison. + if expectedDbusMsg.cmd != resp[1] { + if expectedDbusMsg.cmd == "" { + t.Errorf("expected empty DBUS message cmd, but got: '%v'", resp[1]) + } else { + // Use json.Unmarshal to compare the JSON strings regardless of field order. + var expected, received interface{} + if err := json.Unmarshal([]byte(expectedDbusMsg.cmd), &expected); err != nil { + t.Errorf("failed to unmarshal expected DBUS message's cmd: %v", err) + continue + } + if err := json.Unmarshal([]byte(resp[1]), &received); err != nil { + t.Errorf("failed to unmarshal received DBUS message's cmd: %v", err) + continue + } + if !reflect.DeepEqual(expected, received) { + t.Errorf("expected DBUS message's cmd: '%v' but got '%v'", expectedDbusMsg.cmd, resp[1]) + continue + } + } + } + case <-time.After(time.Second * 5): + t.Errorf("did not get expected DBUS message and/or clientAction() did not finish within 5s") + } + } + + // Wait for the clientAction() to finish. + select { + case err := <-finished: + log.V(2).Infof("clientAction() is done with err=%v\n", err) + if err != nil { + t.Error(err.Error()) + } + case <-time.After(time.Second * 5): + t.Errorf("clientAction() did not finish within 5s") + } + + // Check for any unexpected messages. + select { + case resp := <-dbusListener: + t.Errorf("received unexpected DBUS message: %v", resp) + default: + } + log.V(2).Infoln("Finished:", testName) + }) +} + +// mockDBusMessage is a struct that represents a DBUS message. +type mockDBusMessage struct { + methodName string // endpoint of the host service (intName) + cmd string // content of the DBUS message in JSON-formatted string +} + +// CONSOLE + +var consoleTests = []struct { + desc string + f func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) +}{ + { + desc: "two accounts, finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + if c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Password{ + Password: &credz.PasswordRequest{ + Accounts: []*credz.PasswordRequest_Account{ + { + Account: "alice", + Password: &credz.PasswordRequest_Password{ + Value: &credz.PasswordRequest_Password_Plaintext{ + Plaintext: "password-alice"}}, + Version: "version-1", + CreatedOn: 123, + }, + { + Account: "bob", + Password: &credz.PasswordRequest_Password{ + Value: &credz.PasswordRequest_Password_Plaintext{ + Plaintext: "password-bob"}}, + Version: "version-2", + CreatedOn: 321, + }, + }, + }, + }, + }); err != nil { + t.Fatal(err) + } + if resp, err := c.Recv(); err != nil || resp.GetResponse() == nil { + t.Fatalf("expected Response; err: %v; resp: %v", err, resp) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleCreateCmd, dbus) + } + + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleSetCmd, `{ "ConsolePasswords": [ { "name": "alice", "password" : "password-alice" },{ "name": "bob", "password" : "password-bob" } ] }`}) { + t.Fatalf("DBUS Failure wanted gnsi_console.set; got: %v", dbus) + } + if err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Finalize{}, + }); err != nil { + t.Fatal(err) + } + if resp, err := c.Recv(); err != io.EOF || resp.GetResponse() != nil { + t.Fatalf("expected EOF; err: %v; resp: %v", err, resp) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleDeleteCmd, ""}) { + t.Fatalf("DBUS Failure wanted gnsi_console.delete_checkpoint; got: %v", dbus) + } + }, + }, + { + desc: "two accounts, no finalize", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Password{ + Password: &credz.PasswordRequest{ + Accounts: []*credz.PasswordRequest_Account{ + { + Account: "alice", + Password: &credz.PasswordRequest_Password{ + Value: &credz.PasswordRequest_Password_Plaintext{ + Plaintext: "password-alice"}}, + Version: "version-1", + CreatedOn: 123, + }, + { + Account: "bob", + Password: &credz.PasswordRequest_Password{ + Value: &credz.PasswordRequest_Password_Plaintext{ + Plaintext: "password-bob"}}, + Version: "version-2", + CreatedOn: 321, + }, + }, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + resp, err := c.Recv() + if err != nil { + t.Fatal(err) + } + if resp.GetResponse() == nil { + t.Fatal("Expected a message.") + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleCreateCmd, dbus) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleSetCmd, `{ "ConsolePasswords": [ { "name": "alice", "password" : "password-alice" },{ "name": "bob", "password" : "password-bob" } ] }`}) { + t.Fatalf("DBUS Failure wanted gnsi_console.set; got: %v", dbus) + } + if err = c.CloseSend(); err != nil { + t.Fatal(err) + } + resp, err = c.Recv() + if err == nil { + t.Fatal("Expected an error but did not get it") + } + if status.Code(err) != codes.Aborted { + t.Fatal(err) + } + if resp != nil { + t.Fatal("Received unexpected message after closing connection.") + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleRestoreCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleRestoreCmd, dbus) + } + }, + }, + { + desc: "incomplete set request (password), connection closed", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Password{ + Password: &credz.PasswordRequest{ + Accounts: []*credz.PasswordRequest_Account{ + { + Account: "alice", + Version: "version-1", + CreatedOn: 123, + }, + }, + }, + }, + }); err != nil { + t.Fatal(err) + } + if resp, err := c.Recv(); status.Code(err) != codes.Aborted { + t.Fatalf("expected Aborted; err: %v; resp: %v", err, resp) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleCreateCmd, dbus) + } + }, + }, + { + desc: "incomplete set request (password blank), connection closed", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Password{ + Password: &credz.PasswordRequest{ + Accounts: []*credz.PasswordRequest_Account{ + { + Account: "alice", + Version: "version-1", + CreatedOn: 123, + Password: &credz.PasswordRequest_Password{ + Value: &credz.PasswordRequest_Password_Plaintext{ + Plaintext: ""}}, + }, + }, + }, + }, + }); err != nil { + t.Fatal(err) + } + if resp, err := c.Recv(); status.Code(err) != codes.Aborted { + t.Fatalf("expected Aborted; err: %v; resp: %v", err, resp) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleRestoreCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleRestoreCmd, dbus) + } + }, + }, + { + desc: "incomplete set request (username), connection closed", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Password{ + Password: &credz.PasswordRequest{ + Accounts: []*credz.PasswordRequest_Account{ + { + Password: &credz.PasswordRequest_Password{ + Value: &credz.PasswordRequest_Password_Plaintext{ + Plaintext: "alice-password"}}, + Version: "version-1", + CreatedOn: 123, + }, + }, + }, + }, + }); err != nil { + t.Fatal(err) + } + _, err = c.Recv() + if err == nil { + t.Fatal("Expected an error but did not get it") + } + if status.Code(err) != codes.Aborted { + t.Fatal(err) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleCreateCmd, dbus) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleRestoreCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleRestoreCmd, dbus) + } + }, + }, + { + desc: "incomplete set request (version), connection closed", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Password{ + Password: &credz.PasswordRequest{ + Accounts: []*credz.PasswordRequest_Account{ + { + Account: "alice", + Password: &credz.PasswordRequest_Password{ + Value: &credz.PasswordRequest_Password_Plaintext{ + Plaintext: "alice-password"}}, + CreatedOn: 123, + }, + }, + }, + }, + }); err != nil { + t.Fatal(err) + } + _, err = c.Recv() + if err == nil { + t.Fatal("Expected an error but did not get it") + } + if status.Code(err) != codes.Aborted { + t.Fatal(err) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleCreateCmd, dbus) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleRestoreCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleRestoreCmd, dbus) + } + }, + }, + { + desc: "incomplete set request (created_on), connection closed", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + if err = c.Send(&credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Password{ + Password: &credz.PasswordRequest{ + Accounts: []*credz.PasswordRequest_Account{ + { + Account: "alice", + Password: &credz.PasswordRequest_Password{ + Value: &credz.PasswordRequest_Password_Plaintext{ + Plaintext: "alice-password"}}, + Version: "version-1", + }, + }, + }, + }, + }); err != nil { + t.Fatal(err) + } + _, err = c.Recv() + if err == nil { + t.Fatal("Expected an error but did not get it") + } + if status.Code(err) != codes.Aborted { + t.Fatal(err) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleCreateCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleCreateCmd, dbus) + } + if dbus := dbusListen(ch); !reflect.DeepEqual(dbus, []string{expectedConsoleRestoreCmd, ""}) { + t.Fatalf("DBUS Failure wanted: [%v ]; got: %+v", expectedConsoleRestoreCmd, dbus) + } + }, + }, + { + desc: "no accounts, no finalize, abrupt close", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatal(err) + } + + // Trigger the close + cancel() + + resp, err := c.Recv() + if err == nil { + t.Fatal("Expected an error but did not get it") + } + // Check for Canceled or DeadlineExceeded + code := status.Code(err) + if code != codes.Canceled && code != codes.DeadlineExceeded { + t.Fatalf("Unexpected error code: %v; err: %v", code, err) + } + if resp != nil { + t.Fatal("Received unexpected message after closing connection.") + } + select { + case msg := <-ch: + if msg[0] != expectedConsoleCreateCmd && msg[0] != expectedConsoleRestoreCmd { + t.Errorf("Unexpected DBUS msg %v", msg) + } + default: + } + }, + }, + { + desc: "read JSON with version info, fail", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + s := &GNSICredentialzServer{ + consoleCredMetadata: NewConsoleCredMetadata(), + } + if err := s.loadConsoleCredentialFreshness(""); err == nil { + t.Fatal("Expected file read error") + } + }, + }, + { + desc: "read JSON with version info, success", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + s := &GNSICredentialzServer{ + consoleCredMetadata: NewConsoleCredMetadata(), + } + if err := s.loadConsoleCredentialFreshness("../testdata/gnsi/console-version.json"); err != nil { + t.Fatal(err) + } + }, + }, + { + desc: "write JSON with version info, fail", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + s := &GNSICredentialzServer{ + consoleCredMetadata: NewConsoleCredMetadata(), + } + if err := s.saveConsoleCredentialsFreshness(""); err == nil { + t.Fatal("Expected file write error") + } + }, + }, + { + desc: "write JSON with version info, success", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + s := &GNSICredentialzServer{ + consoleCredMetadata: NewConsoleCredMetadata(), + } + if err := s.saveConsoleCredentialsFreshness("../testdata/gnsi/console-version.json"); err != nil { + t.Fatal(err) + } + }, + }, +} + +// TestConsoleServer tests implementation of gnsi.Ssh server. +func TestGnsiCredzConsoleServer(t *testing.T) { + cfg := &Config{Port: 8081} + s := createCredzServer(t, cfg) + go runServer(t, s) + defer s.Stop() + + var dbusListener chan []string + var done chan bool + var resetFunc func() + + dbusListener, done, resetFunc = newMockConsoleDbusServer() + defer resetFunc() + defer func() { done <- true }() + + // Create a gNSI.ssh client and connect it to the gNSI.ssh server. + tlsConfig := &tls.Config{InsecureSkipVerify: true} + opts := []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))} + targetAddr := fmt.Sprintf("127.0.0.1:%d", s.config.Port) + conn, err := grpc.Dial(targetAddr, opts...) + if err != nil { + t.Fatalf("Dialing to %s failed: %v", targetAddr, err) + } + defer conn.Close() + sc := credz.NewCredentialzClient(conn) + for _, tc := range consoleTests { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + t.Run(tc.desc, func(t *testing.T) { + credzMu.Lock() + credzMu.Unlock() + tc.f(t, ctx, sc, dbusListener) + }) + cancel() + } + + done <- true +} + +var consoleTestsBadDBUS = []struct { + desc string + f func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) +}{ + { + desc: "RPC fails", + f: func(t *testing.T, ctx context.Context, sc credz.CredentialzClient, ch <-chan []string) { + if _, err := sc.RotateAccountCredentials(ctx); err != nil { + t.Fatal(err) + } + select { + case msg := <-ch: + t.Fatalf("Unexpected DBUS msg %v", msg) + default: + } + }, + }, +} + +// TestConsoleServerNoDBUS tests implementation of gnsi.console server. +func TestGnsiCredzConsoleServerNoDBUS(t *testing.T) { + cfg := &Config{Port: 8081} + s := createCredzServer(t, cfg) + go runServer(t, s) + defer s.Stop() + + var dbusListener chan []string + var done chan bool + var resetFunc func() + + dbusListener, done, resetFunc = newFailingConsoleDbusServer() + defer resetFunc() + defer func() { done <- true }() + + // Create a gNSI.ssh client and connect it to the gNSI.ssh server. + tlsConfig := &tls.Config{InsecureSkipVerify: true} + opts := []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))} + targetAddr := fmt.Sprintf("127.0.0.1:%d", s.config.Port) + conn, err := grpc.Dial(targetAddr, opts...) + if err != nil { + t.Fatalf("Dialing to %s failed: %v", targetAddr, err) + } + defer conn.Close() + credzClient := credz.NewCredentialzClient(conn) + + var mu sync.Mutex + for _, tc := range consoleTestsBadDBUS { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + t.Run(tc.desc, func(t *testing.T) { + mu.Lock() + defer mu.Unlock() + tc.f(t, ctx, credzClient, dbusListener) + }) + cancel() + } + + // Shutdown the mock gnsi_console service server. + done <- true + s.gnsiCredentialz.saveConsoleCredentialsFreshness(s.config.ConsoleCredMetaFile) +} + +func newMockConsoleDbusServer() (chan []string, chan bool, func()) { + dbusServerInputChan := make(chan []string, 10) + listenerChan := make(chan []string, 10) + done := make(chan bool, 1) + + // Initialize our FakeClient, directing its output to dbusServerInputChan. + fakeService := &ssc.FakeClient{Command: dbusServerInputChan} + + // 1. Save the original provider + originalProvider := ssc.NewDbusClientProvider + + // 2. Overwrite the provider with our mock + ssc.NewDbusClientProvider = func() (ssc.Service, error) { + return fakeService, nil + } + + // 3. Define the cleanup/reset function + resetFunc := func() { + ssc.NewDbusClientProvider = originalProvider + } + go func() { + checkpoint := false + var mu sync.Mutex + for { + select { + case <-done: // if cancel() execute + log.Infoln("Shutting down the DBUS server.") + return + case msg := <-dbusServerInputChan: + mu.Lock() + fmt.Printf("Received %v. Sending it out.\n", msg) + switch msg[0] { + case expectedConsoleCreateCmd: + if checkpoint { + log.Fatal("mock gnsi_console service: checkpoint already exists.") + } + checkpoint = true + case expectedConsoleDeleteCmd: + if !checkpoint { + log.Fatal("mock gnsi_console service: checkpoint does not exists.") + } + checkpoint = false + case expectedConsoleRestoreCmd: + if !checkpoint { + log.Fatal("mock gnsi_console service: checkpoint does not exists.") + } + checkpoint = false + case expectedConsoleSetCmd: + if !checkpoint { + log.Fatal("mock gnsi_console service: set without checkpoint.") + } + default: + log.Fatalf(`mock gnsi_console service: unknown service: "%v"`, msg[0]) + } + listenerChan <- msg + mu.Unlock() + } + } + }() + + return listenerChan, done, resetFunc +} + +func newFailingConsoleDbusServer() (chan []string, chan bool, func()) { + dbusServerInputChan := make(chan []string, 10) + listenerChan := make(chan []string, 10) + done := make(chan bool, 1) + + // Initialize our FakeClient, directing its output to dbusServerInputChan. + fakeService := &ssc.FakeClient{Command: dbusServerInputChan} + + // 1. Save the original provider + originalProvider := ssc.NewDbusClientProvider + + // 2. Overwrite the provider with our mock + ssc.NewDbusClientProvider = func() (ssc.Service, error) { + return fakeService, nil + } + + // 3. Define the cleanup/reset function + resetFunc := func() { + ssc.NewDbusClientProvider = originalProvider + } + go func() { + for { + select { + case <-done: // if cancel() execute + log.Infoln("Shutting down the faulty DBUS server.") + return + case msg := <-dbusServerInputChan: + fmt.Printf("Received %v. Sending it out.\n", msg) + listenerChan <- msg + } + } + }() + + return listenerChan, done, resetFunc +} + +func TestGnsiCredzUnimplemented(t *testing.T) { + cs := GNSICredentialzServer{} + t.Run("CanGenerateKeyUnimplemented", func(t *testing.T) { + if _, err := cs.CanGenerateKey(nil, nil); status.Code(err) != codes.Unimplemented { + t.Errorf("expected: Unimplemented, got: %+v", err) + } + }) + t.Run("GetPublicKeysUnimplemented", func(t *testing.T) { + if _, err := cs.GetPublicKeys(nil, nil); status.Code(err) != codes.Unimplemented { + t.Errorf("expected: Unimplemented, got: %+v", err) + } + }) +} + +var sshAcctIncompleteMsg = []struct { + desc string + msg *credz.RotateAccountCredentialsRequest +}{ + { + desc: "user; missing version", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{ + User: &credz.AuthorizedUsersRequest{ + Policies: []*credz.UserPolicy{ + { + Account: "root", + // Version: "root-version-2", + CreatedOn: uint64(time.Now().Unix()), + AuthorizedPrincipals: &credz.UserPolicy_SshAuthorizedPrincipals{ + AuthorizedPrincipals: []*credz.UserPolicy_SshAuthorizedPrincipal{ + &credz.UserPolicy_SshAuthorizedPrincipal{ + AuthorizedUser: "alice", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "user; missing users", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{ + User: &credz.AuthorizedUsersRequest{ + Policies: []*credz.UserPolicy{ + { + Account: "root", + Version: "root-version-2", + CreatedOn: uint64(time.Now().Unix()), + AuthorizedPrincipals: &credz.UserPolicy_SshAuthorizedPrincipals{ + AuthorizedPrincipals: []*credz.UserPolicy_SshAuthorizedPrincipal{}, + }, + }, + }, + }, + }, + }, + }, + { + desc: "user; missing user list", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{ + User: &credz.AuthorizedUsersRequest{ + Policies: []*credz.UserPolicy{ + { + Account: "root", + Version: "root-version-2", + CreatedOn: uint64(time.Now().Unix()), + }, + }, + }, + }, + }, + }, + { + desc: "user;missing account", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{ + User: &credz.AuthorizedUsersRequest{ + Policies: []*credz.UserPolicy{ + { + Version: "root-version-2", + CreatedOn: uint64(time.Now().Unix()), + }, + }, + }, + }, + }, + }, + { + desc: "user; missing timestamp", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{ + User: &credz.AuthorizedUsersRequest{ + Policies: []*credz.UserPolicy{ + { + Account: "root", + Version: "root-version-2", + }, + }, + }, + }, + }, + }, + { + desc: "user; missing user", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_User{}, + }, + }, + { + desc: "cred; missing account", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{ + Credential: &credz.AuthorizedKeysRequest{ + Credentials: []*credz.AccountCredentials{ + { + Version: "root-version-1", + CreatedOn: uint64(time.Now().Unix()), + AuthorizedKeys: []*credz.AccountCredentials_AuthorizedKey{ + { + AuthorizedKey: []byte("Authorized-key #1"), + Options: []*credz.Option{ + { + Key: &credz.Option_Name{Name: "from"}, + Value: "*.sales.example.net,!pc.sales.example.net", + }, + }, + }, + { + AuthorizedKey: []byte("Authorized-key #2"), + KeyType: credz.KeyType_KEY_TYPE_UNSPECIFIED, + Description: "test#2", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "cred; missing keys #1", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{ + Credential: &credz.AuthorizedKeysRequest{ + Credentials: []*credz.AccountCredentials{ + { + Account: "root", + Version: "root-version-1", + CreatedOn: uint64(time.Now().Unix()), + AuthorizedKeys: []*credz.AccountCredentials_AuthorizedKey{}, + }, + }, + }, + }, + }, + }, + { + desc: "cred; missing cred", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{}, + }, + }, + { + desc: "cred; missing timestamp", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{ + Credential: &credz.AuthorizedKeysRequest{ + Credentials: []*credz.AccountCredentials{ + { + Account: "root", + Version: "root-version-1", + AuthorizedKeys: []*credz.AccountCredentials_AuthorizedKey{ + {AuthorizedKey: []byte("Authorized-key #2")}, + }, + }, + }, + }, + }, + }, + }, + { + desc: "cred; missing account", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{ + Credential: &credz.AuthorizedKeysRequest{ + Credentials: []*credz.AccountCredentials{ + { + Version: "root-version-1", + CreatedOn: uint64(time.Now().Unix()), + AuthorizedKeys: []*credz.AccountCredentials_AuthorizedKey{ + {AuthorizedKey: []byte("Authorized-key #2")}, + }, + }, + }, + }, + }, + }, + }, + { + desc: "cred; missing version", + msg: &credz.RotateAccountCredentialsRequest{ + Request: &credz.RotateAccountCredentialsRequest_Credential{ + Credential: &credz.AuthorizedKeysRequest{ + Credentials: []*credz.AccountCredentials{ + { + Account: "root", + CreatedOn: uint64(time.Now().Unix()), + }, + }, + }, + }, + }, + }, +} + +var sshHostIncompleteMsg = []struct { + desc string + msg *credz.RotateHostParametersRequest +}{ + { + desc: "host missing request", + msg: &credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{}, + }, + }, + { + desc: "host missing keys #1", + msg: &credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{ + SshCaPublicKey: &credz.CaPublicKeyRequest{ + SshCaPublicKeys: []*credz.PublicKey{&credz.PublicKey{ + PublicKey: []byte{}, + KeyType: credz.KeyType_KEY_TYPE_UNSPECIFIED, + Description: "test", + }}, + Version: "CA-trust-bundle-1", + CreatedOn: uint64(time.Now().Unix()), + }, + }, + }, + }, + { + desc: "host missing timestamp", + msg: &credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{ + SshCaPublicKey: &credz.CaPublicKeyRequest{ + Version: "CA-trust-bundle-1", + CreatedOn: uint64(time.Now().Unix()), + }, + }, + }, + }, + { + desc: "host missing version", + msg: &credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{ + SshCaPublicKey: &credz.CaPublicKeyRequest{ + Version: "CA-trust-bundle-1", + CreatedOn: uint64(time.Now().Unix()), + }, + }, + }, + }, +} + +func TestGnsiCredzMissingRequests(t *testing.T) { + cfg := &Config{Port: 8081} + s := createCredzServer(t, cfg) + go runServer(t, s) + defer s.Stop() + + var dbusListener chan []string + var done chan bool + var resetFunc func() + + dbusListener, done, resetFunc = newMockSshDbusServer() + defer resetFunc() + defer func() { done <- true }() + + // Create a gNSI.ssh client and connect it to the gNSI.ssh server. + tlsConfig := &tls.Config{InsecureSkipVerify: true} + opts := []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))} + targetAddr := fmt.Sprintf("127.0.0.1:%d", s.config.Port) + conn, err := grpc.Dial(targetAddr, opts...) + if err != nil { + t.Fatalf("Dialing to %s failed: %v", targetAddr, err) + } + defer conn.Close() + sc := credz.NewCredentialzClient(conn) + + for _, m := range sshAcctIncompleteMsg { + t.Run(m.desc, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + c, err := sc.RotateAccountCredentials(ctx) + if err != nil { + t.Fatalf("error opening a streaming RotateAccountCredentials RPC: %v", err) + } + if err = c.Send(m.msg); err != nil { + t.Fatalf("error sending an incomplete '%v'message: %v", m.desc, err) + } + if _, err := c.Recv(); status.Code(err) != codes.Aborted { + t.Errorf("expected: Aborted, got: %+v", err) + } + if dbus := dbusListen(dbusListener); !reflect.DeepEqual(dbus, []string{expectedSshCreateCmd, ""}) { + t.Errorf("DBUS Failure wanted: [%v ]; got: %+v", expectedSshCreateCmd, dbus) + } + if dbus := dbusListen(dbusListener); !reflect.DeepEqual(dbus, []string{expectedSshRestoreCmd, ""}) { + t.Errorf("DBUS Failure wanted %v; got: %v", expectedSshRestoreCmd, dbus) + } + }) + } + for _, m := range sshHostIncompleteMsg { + t.Run(m.desc, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + c, err := sc.RotateHostParameters(ctx) + if err != nil { + t.Fatalf("error opening a streaming RotateHostParameters RPC: %v", err) + } + if err = c.Send(m.msg); err != nil { + t.Fatalf("error sending an incomplete '%v'message: %v", m.desc, err) + } + if _, err := c.Recv(); status.Code(err) != codes.Aborted { + t.Errorf("expected: Aborted, got: %+v", err) + } + if dbus := dbusListen(dbusListener); !reflect.DeepEqual(dbus, []string{expectedSshCreateCmd, ""}) { + t.Errorf("DBUS Failure wanted: [%v ]; got: %+v", expectedSshCreateCmd, dbus) + } + if dbus := dbusListen(dbusListener); !reflect.DeepEqual(dbus, []string{expectedSshRestoreCmd, ""}) { + t.Errorf("DBUS Failure wanted %v; got: %v", expectedSshRestoreCmd, dbus) + } + }) + } +} + +// glomeTests contains the test cases for valid and invalid Glome transactions. + +var glomeTests = []struct { + desc string + f func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) +}{ + { + // Valid scenario where a client sends a GlomeRequest to enable GLOME followed by a Finalize request. + // Stream is opened, GlomeRequest is sent which triggers a DBus call with 'push_config' cmd, + // FinalizeRequest is sent committing the transaction, then the stream is closed. + desc: "GLOME scenario: glome enable, finalize", + f: func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) { + var rhpClient credz.Credentialz_RotateHostParametersClient + var err error + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + rhpClient, err = credzClient.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("GlomeRequest", ch, t, + []mockDBusMessage{glomePushConfigDbusMessageForValidRequest}, + func() error { + if err = rhpClient.Send(validRotateHostParametersGlomeRequest); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + resp, err := rhpClient.Recv() + if err != nil { + t.Fatalf("rhpClient.Recv() failed; err: %v", err) + } + if resp.GetGlome() == nil { + t.Fatal("resp.GetGlome() is nil; expected a GlomeResponse") + } + return nil + }) + runRotateHostParameters("FinalizeRequest", ch, t, []mockDBusMessage{}, func() error { + if err = rhpClient.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Finalize{}}); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + // Check that the client receives an EOF after the Finalize request. + if resp, err := rhpClient.Recv(); err != io.EOF || resp.GetResponse() != nil { + t.Errorf("rhpClient.Recv() returned resp: %v, err: %v; expected no response and error: %v", resp, err, io.EOF) + } + return nil + }) + }, + }, + { + // Valid scenario where a client sends a GlomeRequest to disable GLOME followed by a Finalize request. + // Stream is opened, GlomeRequest is sent which triggers a DBus call with 'push_config' cmd, + // FinalizeRequest is sent committing the transaction, then the stream is closed. + desc: "GLOME scenario: glome disable, finalize", + f: func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) { + var rhpClient credz.Credentialz_RotateHostParametersClient + var err error + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + rhpClient, err = credzClient.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("GlomeRequest", ch, t, + []mockDBusMessage{glomePushConfigDbusMessageForDisableRequest}, + func() error { + if err = rhpClient.Send(rotateHostParametersDisableGlomeRequest); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + resp, err := rhpClient.Recv() + if err != nil { + t.Fatalf("rhpClient.Recv() failed; err: %v", err) + } + if resp.GetGlome() == nil { + t.Fatal("resp.GetGlome() is nil; expected a GlomeResponse") + } + return nil + }) + runRotateHostParameters("FinalizeRequest", ch, t, []mockDBusMessage{}, func() error { + if err = rhpClient.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Finalize{}}); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + // Check that the client receives an EOF after the Finalize request. + if resp, err := rhpClient.Recv(); err != io.EOF || resp.GetResponse() != nil { + t.Errorf("rhpClient.Recv() returned resp: %v, err: %v; expected no response and error: %v", resp, err, io.EOF) + } + return nil + }) + }, + }, + { + // Invalid scenario where a client sends a GlomeRequest but not a Finalize request before closing the stream. + // Stream is opened, GlomeRequest is sent which triggers a DBus call with 'push_config' cmd, + // Stream is closed without a Finalize request, the server aborts the transaction and sends 'restore_checkpoint' DBus message, + // the server returns an Aborted error to the client. + desc: "GLOME scenario: glome, no finalize", + f: func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) { + var rhpClient credz.Credentialz_RotateHostParametersClient + var err error + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + rhpClient, err = credzClient.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("GlomeRequest", ch, t, + []mockDBusMessage{glomePushConfigDbusMessageForValidRequest}, + func() error { + if err = rhpClient.Send(validRotateHostParametersGlomeRequest); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + resp, err := rhpClient.Recv() + if err != nil { + t.Fatalf("rhpClient.Recv() failed; err: %v", err) + } + if resp.GetGlome() == nil { + t.Fatal("resp.GetGlome() is nil; expected a GlomeResponse") + } + return nil + }) + runRotateHostParameters("CloseStream", ch, t, []mockDBusMessage{glomeRestoreDbusMessage}, + func() error { + if err = rhpClient.CloseSend(); err != nil { + t.Fatalf("rhpClient.CloseSend() failed; err: %v", err) + } + // Check that the client receives an Aborted error from the server due to closing the stream without a Finalize request. + if resp, err := rhpClient.Recv(); status.Code(err) != codes.Aborted || resp != nil { + t.Errorf("rhpClient.Recv() returned resp: %v, err: %v; expected error status code: %v", resp, err, codes.Aborted) + } + return nil + }) + }, + }, + { + // Invalid scenario where a client sends a Finalize request without a Glome request. + // Stream is opened, FinalizeRequest is sent, the server aborts the transaction and sends 'restore_checkpoint' DBus message, + // the server returns an Aborted error to the client. + desc: "GLOME scenario: no glome, finalize", + f: func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) { + var rhpClient credz.Credentialz_RotateHostParametersClient + var err error + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + rhpClient, err = credzClient.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("FinalizeRequest", ch, t, []mockDBusMessage{}, + func() error { + if err = rhpClient.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Finalize{}}); err != nil { + //t.Fatal("rhpClient.Send() failed; err: %v", err) + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + // Check that the client receives an Aborted error due to receiving a Finalize request without a Glome request. + if resp, err := rhpClient.Recv(); status.Code(err) != codes.Aborted || resp != nil { + t.Errorf("rhpClient.Recv() returned resp: %v, err: %v; expected error status code: %v", resp, err, codes.Aborted) + } + return nil + }) + }, + }, + { + // Invalid scenario where a client sends a second GlomeRequest after the first one. + // Stream is opened, GlomeRequest is sent which triggers a DBus call with 'push_config' cmd, + // Second GlomeRequest is sent, the server aborts the transaction and sends 'restore_checkpoint' DBus message, + // the server returns an Aborted error to the client. + desc: "GLOME scenario: glome, glome", + f: func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) { + var rhpClient credz.Credentialz_RotateHostParametersClient + var err error + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + rhpClient, err = credzClient.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("GlomeRequest", ch, t, + []mockDBusMessage{glomePushConfigDbusMessageForValidRequest}, + func() error { + if err = rhpClient.Send(validRotateHostParametersGlomeRequest); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + resp, err := rhpClient.Recv() + if err != nil { + t.Fatalf("rhpClient.Recv() failed; err: %v", err) + } + if resp.GetGlome() == nil { + t.Fatal("resp.GetGlome() is nil; expected a GlomeResponse") + } + return nil + }) + runRotateHostParameters("SecondGlomeRequest", ch, t, []mockDBusMessage{glomeRestoreDbusMessage}, + func() error { + if err = rhpClient.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Glome{ + Glome: &credz.GlomeRequest{ + Enabled: true, + Key: "test-key-2", + KeyVersion: 2, + UrlPrefix: "https://test.com/", + }, + }, + }); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + // Check that the client receives an Aborted error due to another Glome request after the first one. The server expects + // Finalize request after the first Glome request. + if resp, err := rhpClient.Recv(); status.Code(err) != codes.Aborted || resp != nil { + t.Errorf("rhpClient.Recv() returned resp: %v, err: %v; expected error status code: %v", resp, err, codes.Aborted) + } + return nil + }) + }, + }, + { + // Invalid scenario where a client sends a GlomeRequest followed by a SshCaPublicKey request. + // Stream is opened, GlomeRequest is sent which triggers a DBus call with 'push_config' cmd, + // SshCaPublicKey request is sent, the server aborts the transaction and sends 'restore_checkpoint' DBus message, + // the server returns an Aborted error to the client. + desc: "GLOME scenario: glome, sshCaPublicKey", + f: func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) { + var rhpClient credz.Credentialz_RotateHostParametersClient + var err error + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + rhpClient, err = credzClient.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("GlomeRequest", ch, t, + []mockDBusMessage{glomePushConfigDbusMessageForValidRequest}, + func() error { + if err = rhpClient.Send(validRotateHostParametersGlomeRequest); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + resp, err := rhpClient.Recv() + if err != nil { + t.Fatalf("rhpClient.Recv() failed; err: %v", err) + } + if resp.GetGlome() == nil { + t.Fatal("resp.GetGlome() is nil; expected a GlomeResponse") + } + return nil + }) + runRotateHostParameters("ModifySshCaPublicKey", ch, t, + []mockDBusMessage{glomeRestoreDbusMessage}, + func() error { + if err = rhpClient.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_SshCaPublicKey{ + SshCaPublicKey: &credz.CaPublicKeyRequest{ + Version: "CA-trust-bundle-1", + CreatedOn: 123, + SshCaPublicKeys: []*credz.PublicKey{ + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #1"), + KeyType: credz.KeyType_KEY_TYPE_ED25519, + Description: "test#1", + }, + &credz.PublicKey{ + PublicKey: []byte("TEST-CERT #2"), + KeyType: credz.KeyType_KEY_TYPE_RSA_2048, + Description: "test#2", + }, + }, + }, + }, + }); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + // Check that the client receives an Aborted error due to SSH CA public key request after Glome request. + if resp, err := rhpClient.Recv(); status.Code(err) != codes.Aborted || resp != nil { + t.Errorf("rhpClient.Recv() returned resp: %v, err: %v; expected error status code: %v", resp, err, codes.Aborted) + } + return nil + }) + }, + }, + { + desc: "GLOME scenario: invalid url_prefix in glome request", // Invalid case. + f: func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) { + var rhpClient credz.Credentialz_RotateHostParametersClient + var err error + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + rhpClient, err = credzClient.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("GlomeRequest", ch, t, + []mockDBusMessage{glomeRestoreDbusMessage}, + func() error { + if err = rhpClient.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Glome{ + Glome: &credz.GlomeRequest{ + Enabled: true, + Key: "test-key", + KeyVersion: 1, + UrlPrefix: "%%test.com", + }, + }, + }); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + // Check that the client receives an Aborted error due to invalid url_prefix in glome request. + resp, err := rhpClient.Recv() + if status.Code(err) != codes.Aborted || resp != nil { + t.Errorf("rhpClient.Recv() returned resp: %v, err: %v; expected error status code: %v", resp, err, codes.Aborted) + } + // Check that the error message contains the expected error message. + if !strings.Contains(err.Error(), "GLOME URL prefix is not valid") { + t.Errorf("rhpClient.Recv() returned err: %v; expected error message: %v", err, "GLOME URL prefix is not valid") + } + return nil + }) + }, + }, + { + desc: "GLOME scenario: glome enabled false, but other fields are set", // Invalid case. + f: func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) { + var rhpClient credz.Credentialz_RotateHostParametersClient + var err error + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + rhpClient, err = credzClient.RotateHostParameters(ctx) + return err + }) + runRotateHostParameters("GlomeRequest", ch, t, + []mockDBusMessage{glomeRestoreDbusMessage}, + func() error { + if err = rhpClient.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Glome{ + Glome: &credz.GlomeRequest{ + Enabled: false, + Key: "test-key", + KeyVersion: 1, + UrlPrefix: "%%test.com", + }, + }, + }); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + // Check that the client receives an Aborted error due to invalid url_prefix in glome request. + resp, err := rhpClient.Recv() + if status.Code(err) != codes.Aborted || resp != nil { + t.Errorf("rhpClient.Recv() returned resp: %v, err: %v; expected error status code: %v", resp, err, codes.Aborted) + } + // Check that the error message contains the expected error message. + if !strings.Contains(err.Error(), "GLOME key, key_version, and url_prefix cannot be set if GLOME is disabled") { + t.Errorf("rhpClient.Recv() returned err: %v; expected error message: %v", err, "GLOME key, key_version, and url_prefix cannot be set if GLOME is disabled") + } + return nil + }) + }, + }, + { + // This test case is to ensure that the proto JSON marshaling of the GlomeRequest works correctly + // with special characters that need escaping. + desc: "GLOME scenario: proto json marshaling with specical characters", + f: func(t *testing.T, ctx context.Context, credzClient credz.CredentialzClient, ch <-chan []string) { + var rhpClient credz.Credentialz_RotateHostParametersClient + var err error + runRotateHostParameters("OpenStream", ch, t, []mockDBusMessage{}, func() error { + rhpClient, err = credzClient.RotateHostParameters(ctx) + return err + }) + + // GlomeRequest with special characters in the url_prefix field. + glomeRequestWithSpecialChars := &credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Glome{ + Glome: &credz.GlomeRequest{ + Enabled: true, + Key: "test-key", + KeyVersion: 1, + UrlPrefix: "https://example.com/?q=\"value\"", + }, + }, + } + + // The expected JSON string after marshaling and compacting. Quotes and + // backslashes in the url_prefix field should be escaped. + expectedDbusMessageWithSpecialChars := mockDBusMessage{ + methodName: expectedGlomePushConfigCmd, + cmd: `{"enabled":true,"key":"test-key","key_version":1,"url_prefix":"https://example.com/?q=\"value\""}`, + } + + runRotateHostParameters("GlomeRequestWithSpecialCharacters", ch, t, []mockDBusMessage{expectedDbusMessageWithSpecialChars}, + func() error { + if err = rhpClient.Send(glomeRequestWithSpecialChars); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + resp, err := rhpClient.Recv() + if err != nil { + t.Fatalf("rhpClient.Recv() failed; err: %v", err) + } + if resp.GetGlome() == nil { + t.Fatal("resp.GetGlome() is nil; expected a GlomeResponse") + } + return nil + }) + + runRotateHostParameters("FinalizeRequest", ch, t, []mockDBusMessage{}, func() error { + if err = rhpClient.Send(&credz.RotateHostParametersRequest{ + Request: &credz.RotateHostParametersRequest_Finalize{}}); err != nil { + t.Fatalf("rhpClient.Send() failed; err: %v", err) + } + // Check that the client receives an EOF after the Finalize request. + if resp, err := rhpClient.Recv(); err != io.EOF || resp.GetResponse() != nil { + t.Errorf("rhpClient.Recv() returned resp: %v, err: %v; expected no response and error: %v", resp, err, io.EOF) + } + return nil + }) + }, + }, +} + +// TestGnsiCredzGlome tests the valid and invalid Glome use cases. The only +// valid case is Glome request -> Finalize request. All other cases are invalid +// and should return an Aborted error. +func TestGnsiCredzGlome(t *testing.T) { + cfg := &Config{Port: 8081} + s := createCredzServer(t, cfg) + go runServer(t, s) + defer s.Stop() + + var dbusListener chan []string + var done chan bool + var resetFunc func() + + dbusListener, done, resetFunc = newMockGlomeDbusServer() + defer resetFunc() + defer func() { done <- true }() + + // Create a credz client and connect it to the gNSI Glome service. + tlsConfig := &tls.Config{InsecureSkipVerify: true} + targetAddr := fmt.Sprintf("127.0.0.1:%d", s.config.Port) + conn, err := grpc.Dial(targetAddr, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) + if err != nil { + t.Fatalf("Dialing to %s failed: %v", targetAddr, err) + } + defer conn.Close() + credzClient := credz.NewCredentialzClient(conn) + + for _, tc := range glomeTests { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Run(tc.desc, func(t *testing.T) { + tc.f(t, ctx, credzClient, dbusListener) + credzMu.Lock() + credzMu.Unlock() + }) + cancel() + } + + // Shutdown the mock Glome DBUS server. + done <- true +} + +// newMockGlomeDbusServer creates a mock DBUS server for Glome that runs in the background as +// a goroutine. It returns: +// - listenerChan: A channel that the test function listens on to receive and verify the DBUS messages. +// - shutdownChan: A channel that is used to signal the mock DBUS server to shutdown. +// - dbusCaller: A spy caller that the gNSI server uses to send DBUS messages to the mock DBUS server. +func newMockGlomeDbusServer() (chan []string, chan bool, func()) { + // dbusServerInputChan is a channel that the code under test (the gNSI server) sends the DBUS messages to. + dbusServerInputChan := make(chan []string, 10) + // listenerChan is a channel that the test function listens on to receive and verify the DBUS messages. + listenerChan := make(chan []string, 10) + // shutdownChan is used to signal the mock Dbus server's goroutine to shutdown. + shutdownChan := make(chan bool, 1) + + // Initialize our FakeClient, directing its output to dbusServerInputChan. + fakeService := &ssc.FakeClient{Command: dbusServerInputChan} + + // 1. Save the original provider + originalProvider := ssc.NewDbusClientProvider + + // 2. Overwrite the provider with our mock + ssc.NewDbusClientProvider = func() (ssc.Service, error) { + return fakeService, nil + } + + // 3. Define the cleanup/reset function + resetFunc := func() { + ssc.NewDbusClientProvider = originalProvider + } + go func() { + glomeCheckpoint := false + for { + select { + case <-shutdownChan: // if shutdownChan is signaled from TestGnsiCredzGlome() + //log.V(lvl.INFO).Infoln("Shutting down the DBUS server.") + return + case msg := <-dbusServerInputChan: + //log.V(lvl.INFO).Infof("DBUS service received %+v", msg) + switch msg[0] { + case expectedGlomePushConfigCmd: + glomeCheckpoint = true + case expectedGlomeConfigRestoreCmd: + if !glomeCheckpoint { + log.Fatal("mock glome service: checkpoint does not exists.") + } + default: + //log.V(lvl.INFO).Infof("mock glome service: unknown service: %+v", msg) + } + listenerChan <- msg + } + } + }() + return listenerChan, shutdownChan, resetFunc +} + +// TestReadGlomeConfigMetadataFromStateDB tests STATE_DB read for GlomeConfigMetadata. +// The read function is expected to return: +// - GlomeConfigMetadata exists in STATE_DB, return the data. +// - GlomeConfigMetadata does not exist in STATE_DB, return defaultGlomeConfigMetadata. +// - Redis error reading from STATE_DB, return error. +func TestReadGlomeConfigMetadataFromStateDB(t *testing.T) { + tests := []struct { + name string + dbResult map[string]string + dbErr error + want *GlomeConfigMetadata + wantErr bool + }{ + { + name: "glome config metadata exists in STATE_DB", + dbResult: map[string]string{ + "enabled": "true", + "key_version": "1", + "last_updated": "1234567890", + }, + want: &GlomeConfigMetadata{ + Enabled: true, + KeyVersion: 1, + LastUpdated: 1234567890, + }, + wantErr: false, + }, + { + name: "no glome config metadata in STATE_DB", + dbResult: map[string]string{}, + want: defaultGlomeConfigMetadata, + wantErr: false, + }, + { + name: "error reading from STATE_DB", + dbErr: fmt.Errorf("error reading from STATE_DB"), + wantErr: true, + }, + { + name: "invalid 'enabled' field in STATE_DB", + dbResult: map[string]string{ + "enabled": "invalid", + "key_version": "1", + "last_updated": "1234567890", + }, + wantErr: true, + }, + { + name: "invalid 'key_version' field in STATE_DB", + dbResult: map[string]string{ + "enabled": "true", + "key_version": "invalid", + "last_updated": "1234567890", + }, + wantErr: true, + }, + { + name: "invalid 'last_updated' field in STATE_DB", + dbResult: map[string]string{ + "enabled": "true", + "key_version": "1", + "last_updated": "invalid", + }, + wantErr: true, + }, + } + + for _, test := range tests { + test := test // Capture range variable to avoid race conditions. + t.Run(test.name, func(t *testing.T) { + // Create redismock client + mockClient, mock := redismock.NewClientMock() + srv := &GNSICredentialzServer{stateDbClient: mockClient} + + // Set expectation for HGetAll call. + expected := mock.ExpectHGetAll(stateDbKeyForGlome) + if test.dbErr != nil { + // If dbErr is set for the test, tell the mock to return the error. + expected.SetErr(test.dbErr) + } else { + // Otherwise, tell the mock to return the expected dbResult. + expected.SetVal(test.dbResult) + } + + got, err := srv.readGlomeConfigMetadataFromStateDB(context.Background()) + if (err != nil) != test.wantErr { + t.Errorf("readGlomeConfigMetadataFromStateDB() error = %v, but wantErr = %v", err, test.wantErr) + } + + if !test.wantErr { + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("readGlomeConfigMetadataFromStateDB() returned an unexpected diff (-want +got): %v", diff) + } + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("mock expectations were not met: %s", err) + } + }) + } +} + +// TestWriteGlomeConfigMetadataToStateDB tests STATE_DB write for GlomeConfigMetadata. +// The write function is expected to: +// - Write the new GlomeConfigMetadata to STATE_DB. +// - Return error if newGlomeConfigMetadata (data to be written) is nil. +// - Return error if there is Redis error writing to STATE_DB. +func TestWriteGlomeConfigMetadataToStateDB(t *testing.T) { + tests := []struct { + name string + newData *GlomeConfigMetadata + dbErr error + wantErr bool + needMultiGoroutines bool + }{ + { + name: "success", + newData: &GlomeConfigMetadata{ + Enabled: true, + KeyVersion: 1, + LastUpdated: 1234567890, + }, + wantErr: false, + }, + { + name: "newGlomeConfigMetadata is nil", + newData: nil, + wantErr: true, + }, + { + name: "error writing to STATE_DB", + newData: &GlomeConfigMetadata{ + Enabled: true, + KeyVersion: 1, + LastUpdated: 1234567890, + }, + dbErr: fmt.Errorf("error writing to STATE_DB"), + wantErr: true, + }, + { + name: "concurrent writes to STATE_DB are safe", + newData: &GlomeConfigMetadata{ + Enabled: true, + KeyVersion: 1, + LastUpdated: 1234567890, + }, + wantErr: false, + needMultiGoroutines: true, + }, + } + + for _, test := range tests { + test := test // Capture range variable to avoid race conditions. + t.Run(test.name, func(t *testing.T) { + // Create redismock client + mockClient, mock := redismock.NewClientMock() + srv := &GNSICredentialzServer{stateDbClient: mockClient} + + // If needMultiGoroutines is set for the test, run multiple goroutines to test concurrent writes to STATE_DB. + if test.needMultiGoroutines { + var wg sync.WaitGroup + numGoroutines := 10 + for i := 0; i < numGoroutines; i++ { + mock.ExpectHSet(stateDbKeyForGlome, "enabled", test.newData.Enabled, "key_version", test.newData.KeyVersion, "last_updated", test.newData.LastUpdated).SetVal(1) + } + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + err := srv.writeGlomeConfigMetadataToStateDB(context.Background(), test.newData) + if (err != nil) != test.wantErr { + t.Errorf("writeGlomeConfigMetadataToStateDB() error = %v, but wantErr = %v", err, test.wantErr) + } + }() + } + wg.Wait() + } else { + // Otherwise, run the test in a single goroutine. + // Set expectation for HSet call. + if test.newData != nil { + expected := mock.ExpectHSet(stateDbKeyForGlome, "enabled", test.newData.Enabled, "key_version", test.newData.KeyVersion, "last_updated", test.newData.LastUpdated) + if test.dbErr != nil { + // If dbErr is set for the test, tell the mock to return the error. + expected.SetErr(test.dbErr) + } else { + // Otherwise, tell the mock to succeed. + expected.SetVal(1) + } + } + + err := srv.writeGlomeConfigMetadataToStateDB(context.Background(), test.newData) + if (err != nil) != test.wantErr { + t.Errorf("writeGlomeConfigMetadataToStateDB() error = %v, but wantErr = %v", err, test.wantErr) + } + } + + // Verify that all expectations were met. + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("mock expectations were not met: %v", err) + } + }) + } +} diff --git a/gnmi_server/server.go b/gnmi_server/server.go index 4887133f8..7cf2cbf72 100644 --- a/gnmi_server/server.go +++ b/gnmi_server/server.go @@ -44,6 +44,7 @@ import ( gnoi_os_pb "github.com/openconfig/gnoi/os" gnsi_authz_pb "github.com/openconfig/gnsi/authz" gnsi_certz_pb "github.com/openconfig/gnsi/certz" + gnsi_credentialz_pb "github.com/openconfig/gnsi/credentialz" 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" @@ -106,6 +107,8 @@ type Server struct { ConnectionManager *ConnectionManager // DB Journals configDbJournal *DbJournal + gnsiCredentialz *GNSICredentialzServer + gnsi_credentialz_pb.UnimplementedCredentialzServer } // handleOperationalGet handles OPERATIONAL target requests directly with standard gNMI types @@ -244,6 +247,8 @@ type Config struct { PathzPolicyFile string // Path to gNMI pathz policy file. PathzMetaFile string // Path to JSON file with pathz metadata. EnableStreamMultiplexing bool // Allow multiple Subscribe RPCs on a single TCP connection. + SshCredMetaFile string // Path to JSON file with SSH server credential metadata. + ConsoleCredMetaFile string // Path to JSON file with console credential metadata. } // DBusOSBackend is a concrete implementation of OSBackend @@ -334,12 +339,13 @@ 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, authzSrv *GNSIAuthzServer, pathzSrv *GNSIPathzServer) { + debugSrv *DebugServer, healthzSrv *HealthzServer, certzSrv *GNSICertzServer, authzSrv *GNSIAuthzServer, pathzSrv *GNSIPathzServer, credentialzSrv *GNSICredentialzServer) { gnmipb.RegisterGNMIServer(s, srv) factory_reset.RegisterFactoryResetServer(s, srv) gnsi_certz_pb.RegisterCertzServer(s, certzSrv) gnsi_authz_pb.RegisterAuthzServer(s, authzSrv) gnsi_pathz_pb.RegisterPathzServer(s, pathzSrv) + gnsi_credentialz_pb.RegisterCredentialzServer(s, credentialzSrv) spb_jwt_gnoi.RegisterSonicJwtServiceServer(s, srv) if srv.config.EnableTranslibWrite || srv.config.EnableNativeWrite { gnoi_system_pb.RegisterSystemServer(s, srv) @@ -353,7 +359,6 @@ func registerAllServices(s *grpc.Server, srv *Server, fileSrv *FileServer, spb_gnoi.RegisterSonicServiceServer(s, srv) } spb_gnoi.RegisterDebugServer(s, srv) - } // SrvTestConfig returns test mTLS server configuration to be used to start gNMI/gNOI server in test environment. @@ -579,6 +584,8 @@ func NewServer(config *Config, tlsOpts []grpc.ServerOption, commonOpts []grpc.Se certzSrv := NewGNSICertzServer(srv) srv.gnsiCertz = certzSrv + credentialzSrv := NewGNSICredentialzServer(srv) + srv.gnsiCredentialz = credentialzSrv var err error // TCP Server (Port > 0) @@ -598,7 +605,7 @@ func NewServer(config *Config, tlsOpts []grpc.ServerOption, commonOpts []grpc.Se srv.s.Stop() srv.s = nil } else { - registerAllServices(srv.s, srv, fileSrv, osSrv, containerzSrv, debugSrv, healthzSrv, certzSrv, authzSrv, pathzSrv) + registerAllServices(srv.s, srv, fileSrv, osSrv, containerzSrv, debugSrv, healthzSrv, certzSrv, authzSrv, pathzSrv, credentialzSrv) } } @@ -632,7 +639,7 @@ func NewServer(config *Config, tlsOpts []grpc.ServerOption, commonOpts []grpc.Se srv.udsServer.Stop() srv.udsServer = nil } else { - registerAllServices(srv.udsServer, srv, fileSrv, osSrv, containerzSrv, debugSrv, healthzSrv, certzSrv, authzSrv, pathzSrv) + registerAllServices(srv.udsServer, srv, fileSrv, osSrv, containerzSrv, debugSrv, healthzSrv, certzSrv, authzSrv, pathzSrv, credentialzSrv) } } } diff --git a/go.mod b/go.mod index a9ba843ca..04814db22 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module github.com/sonic-net/sonic-gnmi go 1.24.4 - require ( github.com/Azure/sonic-mgmt-common v0.0.0-00010101000000-000000000000 github.com/Workiva/go-datastructures v1.0.50 @@ -10,8 +9,8 @@ require ( github.com/alicebob/miniredis/v2 v2.35.0 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/fsnotify/fsnotify v1.4.9 + github.com/go-redis/redismock/v9 v9.2.0 github.com/godbus/dbus/v5 v5.1.0 github.com/gogo/protobuf v1.3.2 github.com/golang/glog v1.2.4 @@ -26,15 +25,15 @@ require ( github.com/openconfig/gnmi v0.14.1 github.com/openconfig/gnoi v0.3.0 github.com/openconfig/gnsi v1.9.0 - github.com/openconfig/ygot v0.29.20 + github.com/openconfig/ygot v0.7.1 github.com/redis/go-redis/v9 v9.14.1 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.36.0 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.11 - gopkg.in/yaml.v2 v2.2.8 + google.golang.org/protobuf v1.36.6 + gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/sh/v3 v3.8.0 ) @@ -44,16 +43,12 @@ require ( github.com/antchfx/xmlquery v1.3.1 // indirect github.com/antchfx/xpath v1.1.10 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v4 v4.0.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/envoyproxy/go-control-plane v0.12.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect + github.com/go-redis/redis/v7 v7.0.0-beta.3.0.20190824101152-d19aba07b476 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/onsi/ginkgo v1.10.3 // indirect - github.com/onsi/gomega v1.7.1 // indirect github.com/openconfig/goyang v0.0.0-20200309174518-a00bece872fc // indirect github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -64,7 +59,6 @@ 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 ) @@ -74,7 +68,6 @@ replace ( // Glog patch needs to be updated to remove this. github.com/golang/glog => github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/openconfig/gnmi => github.com/openconfig/gnmi v0.0.0-20200617225440-d2b4e6a45802 - github.com/openconfig/ygot => github.com/openconfig/ygot v0.7.1 golang.org/x/crypto => golang.org/x/crypto v0.24.0 golang.org/x/sys => golang.org/x/sys v0.26.0 google.golang.org/grpc => google.golang.org/grpc v1.64.1 diff --git a/go.sum b/go.sum index c87cdca7b..6e2eb37ec 100644 --- a/go.sum +++ b/go.sum @@ -1350,14 +1350,11 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/c9s/goprocinfo v0.0.0-20191125144613-4acdd056c72d h1:MQGrhPHSxg08x+LKgQTOnnjfXt+p+128WCECqAYXJsU= github.com/c9s/goprocinfo v0.0.0-20191125144613-4acdd056c72d/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= +github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU= github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= @@ -1382,7 +1379,6 @@ 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= @@ -1401,7 +1397,6 @@ 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= @@ -1410,7 +1405,6 @@ 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= @@ -1419,9 +1413,9 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= @@ -1446,8 +1440,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/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw= +github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0= 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= @@ -1589,7 +1583,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9K github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= @@ -1647,15 +1640,12 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/msteinert/pam v0.0.0-20201130170657-e61372126161 h1:XQ1+fYPzaWZCVdu1xzjL917Xy9Yb7imLEU0wHelafKA= github.com/msteinert/pam v0.0.0-20201130170657-e61372126161/go.mod h1:np1wUFZ6tyoke22qDJZY40URn9Ae51gX7ljIWXN5TJs= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 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/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 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/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/openconfig/gnmi v0.0.0-20200617225440-d2b4e6a45802 h1:WXFwJlWOJINlwlyAZuNo4GdYZS6qPX36+rRUncLmN8Q= github.com/openconfig/gnmi v0.0.0-20200617225440-d2b4e6a45802/go.mod h1:M/EcuapNQgvzxo1DDXHK4tx3QpYM/uG4l591v33jG2A= github.com/openconfig/gnoi v0.3.0 h1:ieThHVx5rRwAt6lqKOKzoA3pcr5FE5Xs40GJ7wNqshs= @@ -1665,6 +1655,7 @@ github.com/openconfig/gnsi v1.9.0/go.mod h1:mvfo1wUBFfojkHrD8kKqVV8Epoyq1Vt1Qpkj github.com/openconfig/goyang v0.0.0-20200115183954-d0a48929f0ea/go.mod h1:dhXaV0JgHJzdrHi2l+w0fZrwArtXL7jEFoiqLEdmkvU= github.com/openconfig/goyang v0.0.0-20200309174518-a00bece872fc h1:W6XYKuH3mxF5WFhsSQOPPN9DRDba1xz9lbUbQR3uHkg= github.com/openconfig/goyang v0.0.0-20200309174518-a00bece872fc/go.mod h1:dhXaV0JgHJzdrHi2l+w0fZrwArtXL7jEFoiqLEdmkvU= +github.com/openconfig/ygot v0.6.0/go.mod h1:o30svNf7O0xK+R35tlx95odkDmZWS9JyWWQSmIhqwAs= github.com/openconfig/ygot v0.7.1 h1:kqDRYQpowXTr7EhGwr2BBDKJzqs+H8aFYjffYQ8lBsw= github.com/openconfig/ygot v0.7.1/go.mod h1:5MwNX6DMP1QMf2eQjW+aJN/KNslVqRJtbfSL3SO6Urk= github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f h1:WyCn68lTiytVSkk7W1K9nBiSGTSRlUOdyTnSjwrIlok= @@ -1719,8 +1710,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1855,7 +1846,6 @@ 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= @@ -2354,7 +2344,6 @@ 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= @@ -2383,7 +2372,6 @@ 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= @@ -2432,27 +2420,23 @@ google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/examples v0.0.0-20201112215255-90f1b3ee835b h1:NuxyvVZoDfHZwYW9LD4GJiF5/nhiSyP4/InTrvw9Ibk= -google.golang.org/grpc/examples v0.0.0-20201112215255-90f1b3ee835b/go.mod h1:IBqQ7wSUJ2Ep09a8rMWFsg4fmI2r38zwsq8a0GgxXpM= google.golang.org/grpc/security/advancedtls v1.0.0 h1:/KQ7VP/1bs53/aopk9QhuPyFAp9Dm9Ejix3lzYkCrDA= google.golang.org/grpc/security/advancedtls v1.0.0/go.mod h1:o+s4go+e1PJ2AjuQMY5hU82W7lDlefjJA6FqEHRVHWk= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 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= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testdata/gnsi/console-version.json b/testdata/gnsi/console-version.json new file mode 100644 index 000000000..8d80dfd7d --- /dev/null +++ b/testdata/gnsi/console-version.json @@ -0,0 +1 @@ +{"accounts":{}} diff --git a/testdata/gnsi/ssh-version.json b/testdata/gnsi/ssh-version.json new file mode 100644 index 000000000..4564687d1 --- /dev/null +++ b/testdata/gnsi/ssh-version.json @@ -0,0 +1 @@ +{"accounts":{"root":{"keys_version":"root-version-1","keys_created_on":"1","users_version":"root-version-2","users_created_on":"2"}},"host":{"ca_keys_version":"caKeyVersionMeta","ca_keys_created_on":"2"}}