Skip to content

Commit e6c4075

Browse files
committed
feat(BRE2-816): grant/revoke runnable from anywhere
1 parent e5ec5f1 commit e6c4075

13 files changed

Lines changed: 601 additions & 542 deletions

File tree

pkg/cmd/enablessh/enablessh.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ func enableSSH(
8383
reg *register.DeviceRegistration,
8484
brevUser *entity.User,
8585
) error {
86-
u, err := user.Current()
86+
linuxUser, err := user.Current()
8787
if err != nil {
8888
return fmt.Errorf("failed to determine current Linux user: %w", err)
8989
}
90+
linuxUsername := linuxUser.Username
9091

9192
checkSSHDaemon(t)
9293

@@ -95,7 +96,7 @@ func enableSSH(
9596
t.Vprint("")
9697
t.Vprintf(" Node: %s (%s)\n", reg.DisplayName, reg.ExternalNodeID)
9798
t.Vprintf(" Brev user: %s\n", brevUser.ID)
98-
t.Vprintf(" Linux user: %s\n", u.Username)
99+
t.Vprintf(" Linux user: %s\n", linuxUsername)
99100
t.Vprint("")
100101

101102
port, err := register.PromptSSHPort(t)
@@ -107,7 +108,7 @@ func enableSSH(
107108
return fmt.Errorf("enable SSH failed: %w", err)
108109
}
109110

110-
if err := register.GrantSSHAccessToNode(ctx, t, deps.nodeClients, tokenProvider, reg, brevUser, u); err != nil {
111+
if err := register.SetupAndRegisterNodeSSHAccess(ctx, t, deps.nodeClients, tokenProvider, reg, brevUser, linuxUsername); err != nil {
111112
return fmt.Errorf("enable SSH failed: %w", err)
112113
}
113114

pkg/cmd/grantssh/grantssh.go

Lines changed: 39 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ package grantssh
55
import (
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 (
2323
type 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.
3433
type 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

4644
func defaultGrantSSHDeps() grantSSHDeps {
4745
return grantSSHDeps{
48-
platform: register.LinuxPlatform{},
4946
prompter: register.TerminalPrompter{},
5047
nodeClients: register.DefaultNodeClientFactory{},
5148
registrationStore: register.NewFileRegistrationStore(),
5249
}
5350
}
5451

5552
func 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-
220175
func getSelectedUser(usersToSelect []string, selected string, orgMembers []resolvedMember) (*entity.User, error) {
221176
selectedIdx := -1
222177
for i, userSelection := range usersToSelect {

0 commit comments

Comments
 (0)