@@ -5,12 +5,12 @@ package grantssh
55import (
66 "context"
77 "fmt"
8- "os"
9- "os/user"
10- "path/filepath"
11- "strings"
8+
9+ nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
10+ "connectrpc.com/connect"
1211
1312 "github.com/brevdev/brev-cli/pkg/cmd/register"
13+ "github.com/brevdev/brev-cli/pkg/config"
1414 "github.com/brevdev/brev-cli/pkg/entity"
1515 breverrors "github.com/brevdev/brev-cli/pkg/errors"
1616 "github.com/brevdev/brev-cli/pkg/externalnode"
@@ -23,7 +23,6 @@ import (
2323type GrantSSHStore interface {
2424 GetCurrentUser () (* entity.User , error )
2525 GetActiveOrganizationOrDefault () (* entity.Organization , error )
26- GetBrevHomePath () (string , error )
2726 GetAccessToken () (string , error )
2827 GetOrgRoleAttachments (orgID string ) ([]entity.OrgRoleAttachment , error )
2928 GetUserByID (userID string ) (* entity.User , error )
@@ -32,7 +31,6 @@ type GrantSSHStore interface {
3231// grantSSHDeps bundles the side-effecting dependencies of runGrantSSH so they
3332// can be replaced in tests.
3433type grantSSHDeps struct {
35- platform externalnode.PlatformChecker
3634 prompter terminal.Selector
3735 nodeClients externalnode.NodeClientFactory
3836 registrationStore register.RegistrationStore
@@ -45,56 +43,48 @@ type resolvedMember struct {
4543
4644func defaultGrantSSHDeps () grantSSHDeps {
4745 return grantSSHDeps {
48- platform : register.LinuxPlatform {},
4946 prompter : register.TerminalPrompter {},
5047 nodeClients : register.DefaultNodeClientFactory {},
5148 registrationStore : register .NewFileRegistrationStore (),
5249 }
5350}
5451
5552func NewCmdGrantSSH (t * terminal.Terminal , store GrantSSHStore ) * cobra.Command {
53+ var linuxUser string
54+
5655 cmd := & cobra.Command {
5756 Annotations : map [string ]string {"configuration" : "" },
5857 Use : "grant-ssh" ,
5958 DisableFlagsInUseLine : true ,
60- Short : "Grant SSH access to this device for another org member" ,
61- Long : "Grant SSH access to this registered device for another member of your organization. " ,
62- Example : " brev grant-ssh" ,
59+ Short : "Grant SSH access to a node for another org member" ,
60+ Long : "Grant SSH access to a node for another member of your organization" ,
61+ Example : " brev grant-ssh --linux-user ubuntu " ,
6362 RunE : func (cmd * cobra.Command , args []string ) error {
64- return runGrantSSH (cmd .Context (), t , store , defaultGrantSSHDeps ())
63+ if linuxUser == "" {
64+ linuxUser = register .GetCachedLinuxUser ()
65+ }
66+ if linuxUser == "" {
67+ var err error
68+ linuxUser , err = register .PromptLinuxUser (t )
69+ if err != nil {
70+ return fmt .Errorf ("linux user: %w" , err )
71+ }
72+ }
73+ return runGrantSSH (cmd .Context (), t , store , defaultGrantSSHDeps (), linuxUser )
6574 },
6675 }
6776
77+ cmd .Flags ().StringVar (& linuxUser , "linux-user" , "" , "Linux username on the target node" )
78+
6879 return cmd
6980}
7081
71- func runGrantSSH (ctx context.Context , t * terminal.Terminal , s GrantSSHStore , deps grantSSHDeps ) error { //nolint:funlen // grant-ssh flow
72- if ! deps .platform .IsCompatible () {
73- return fmt .Errorf ("brev grant-ssh is only supported on Linux" )
74- }
75-
76- removeCredentialsFile (t , s )
77-
78- reg , err := deps .registrationStore .Load ()
79- if err != nil {
80- return breverrors .WrapAndTrace (err )
81- }
82-
82+ func runGrantSSH (ctx context.Context , t * terminal.Terminal , s GrantSSHStore , deps grantSSHDeps , linuxUser string ) error { //nolint:funlen // grant-ssh flow
8383 currentUser , err := s .GetCurrentUser ()
8484 if err != nil {
8585 return breverrors .WrapAndTrace (err )
8686 }
8787
88- if err := checkSSHEnabled (currentUser .PublicKey ); err != nil {
89- return err
90- }
91-
92- osUser , err := user .Current ()
93- if err != nil {
94- return fmt .Errorf ("failed to determine current Linux user: %w" , err )
95- }
96- linuxUser := osUser .Username
97-
9888 org , err := s .GetActiveOrganizationOrDefault ()
9989 if err != nil {
10090 return breverrors .WrapAndTrace (err )
@@ -103,8 +93,12 @@ func runGrantSSH(ctx context.Context, t *terminal.Terminal, s GrantSSHStore, dep
10393 return fmt .Errorf ("no organization found; please create or join an organization first" )
10494 }
10595
96+ node , err := register .ResolveNode (ctx , deps .prompter , deps .nodeClients , s , deps .registrationStore , org .ID )
97+ if err != nil {
98+ return breverrors .WrapAndTrace (err )
99+ }
100+
106101 orgMembers , err := getOrgMembers (currentUser , t , s , org .ID )
107- // Resolve user details for each member.
108102 if err != nil {
109103 return breverrors .WrapAndTrace (err )
110104 }
@@ -117,7 +111,6 @@ func runGrantSSH(ctx context.Context, t *terminal.Terminal, s GrantSSHStore, dep
117111
118112 selected := deps .prompter .Select ("Select a user to grant SSH access:" , usersToSelect )
119113
120- // Find the selected user.
121114 selectedUser , err := getSelectedUser (usersToSelect , selected , orgMembers )
122115 if err != nil {
123116 return err
@@ -126,47 +119,27 @@ func runGrantSSH(ctx context.Context, t *terminal.Terminal, s GrantSSHStore, dep
126119 t .Vprint ("" )
127120 t .Vprint (t .Green ("Granting SSH access" ))
128121 t .Vprint ("" )
129- t .Vprintf (" Node: %s (%s)\n " , reg .DisplayName , reg .ExternalNodeID )
122+ t .Vprintf (" Node: %s (%s)\n " , node .DisplayName , node .ExternalNodeID )
130123 t .Vprintf (" Brev user: %s (%s)\n " , selectedUser .Name , selectedUser .ID )
131124 t .Vprintf (" Linux user: %s\n " , linuxUser )
132125 t .Vprint ("" )
133126
134- if err := register .GrantSSHAccessToNode (ctx , t , deps .nodeClients , s , reg , selectedUser , osUser ); err != nil {
135- return fmt .Errorf ("grant SSH failed: %w" , err )
136- }
137-
138- t .Vprint (t .Green (fmt .Sprintf ("SSH access granted for %s. They can now SSH to this device via: brev shell %s" , selectedUser .Name , reg .DisplayName )))
139- return nil
140- }
141-
142- // checkSSHEnabled verifies that SSH has been enabled on this device by checking
143- // if the current user's public key is present in authorized_keys.
144- func checkSSHEnabled (currentUserPubKey string ) error {
145- currentUserPubKey = strings .TrimSpace (currentUserPubKey )
146- if currentUserPubKey == "" {
147- return fmt .Errorf ("current user does not have a Brev public key" )
148- }
149-
150- u , err := user .Current ()
151- if err != nil {
152- return fmt .Errorf ("failed to determine current Linux user: %w" , err )
153- }
154-
155- authKeysPath := filepath .Join (u .HomeDir , ".ssh" , "authorized_keys" )
156- existing , err := os .ReadFile (authKeysPath ) // #nosec G304
127+ client := deps .nodeClients .NewNodeClient (s , config .GlobalConfig .GetBrevPublicAPIURL ())
128+ _ , err = client .GrantNodeSSHAccess (ctx , connect .NewRequest (& nodev1.GrantNodeSSHAccessRequest {
129+ ExternalNodeId : node .ExternalNodeID ,
130+ UserId : selectedUser .ID ,
131+ LinuxUser : linuxUser ,
132+ }))
157133 if err != nil {
158- return fmt .Errorf ("failed to read authorized_keys, %w" , err )
159- }
160-
161- if ! strings .Contains (string (existing ), currentUserPubKey ) {
162- return fmt .Errorf ("run 'brev enable-ssh' first" )
134+ return breverrors .WrapAndTrace (err )
163135 }
164136
137+ t .Vprint (t .Green (fmt .Sprintf ("SSH access granted for %s. They can now SSH to this device via: brev shell %s" , selectedUser .Name , node .DisplayName )))
165138 return nil
166139}
167140
168- func getOrgMembers (currentUser * entity.User , t * terminal.Terminal , s GrantSSHStore , orgId string ) ([]resolvedMember , error ) {
169- attachments , err := s .GetOrgRoleAttachments (orgId )
141+ func getOrgMembers (currentUser * entity.User , t * terminal.Terminal , s GrantSSHStore , orgID string ) ([]resolvedMember , error ) {
142+ attachments , err := s .GetOrgRoleAttachments (orgID )
170143 if err != nil {
171144 return nil , fmt .Errorf ("failed to fetch org members: %w" , err )
172145 }
@@ -199,24 +172,6 @@ func getOrgMembers(currentUser *entity.User, t *terminal.Terminal, s GrantSSHSto
199172 return resolved , nil
200173}
201174
202- // removeCredentialsFile removes ~/.brev/credentials.json if it exists.
203- // When granting SSH access to another user, we don't want them to find
204- // the device owner's auth tokens on disk.
205- func removeCredentialsFile (t * terminal.Terminal , s GrantSSHStore ) {
206- brevHome , err := s .GetBrevHomePath ()
207- if err != nil {
208- return
209- }
210- credsPath := filepath .Join (brevHome , "credentials.json" )
211- if err := os .Remove (credsPath ); err != nil {
212- if ! os .IsNotExist (err ) {
213- t .Vprintf (" %s\n " , t .Yellow (fmt .Sprintf ("Warning: failed to remove credentials file: %v\n It is recommended to remove this file yourself so that this user does not see any sensitive tokens:\n rm %s" , err , credsPath )))
214- }
215- return
216- }
217- t .Vprintf (" Removed %s\n " , credsPath )
218- }
219-
220175func getSelectedUser (usersToSelect []string , selected string , orgMembers []resolvedMember ) (* entity.User , error ) {
221176 selectedIdx := - 1
222177 for i , userSelection := range usersToSelect {
0 commit comments