Skip to content

Commit 08b1024

Browse files
authored
fix(BRE2-804): various polish (#315)
* fix(BRE2-804): various polish: name sanitization, interface cleanliness, etc * caching email
1 parent cc084c9 commit 08b1024

14 files changed

Lines changed: 575 additions & 68 deletions

pkg/cmd/cmd.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import (
5858
"github.com/brevdev/brev-cli/pkg/cmd/workspacegroups"
5959
"github.com/brevdev/brev-cli/pkg/cmd/writeconnectionevent"
6060
"github.com/brevdev/brev-cli/pkg/config"
61+
"github.com/brevdev/brev-cli/pkg/entity"
6162
"github.com/brevdev/brev-cli/pkg/featureflag"
6263
"github.com/brevdev/brev-cli/pkg/files"
6364
"github.com/brevdev/brev-cli/pkg/remoteversion"
@@ -257,8 +258,20 @@ func NewBrevCommand() *cobra.Command { //nolint:funlen,gocognit,gocyclo // defin
257258
cmds.SetUsageTemplate(usageTemplate)
258259

259260
// In-memory auth for external node commands — never touches credentials.json.
260-
memAuthStore := store.NewMemoryAuthStore()
261-
memAuthenticator := auth.StandardLogin("", "", nil)
261+
// Pre-fill the cached email so the user sees a confirmation prompt instead of
262+
// having to type it from scratch every time.
263+
cachedEmail, _ := fsStore.GetCachedEmail()
264+
memAuthenticator := auth.StandardLogin("", cachedEmail, nil)
265+
if cachedEmail != "" {
266+
if kas, ok := memAuthenticator.(auth.KasAuthenticator); ok {
267+
kas.ShouldPromptEmail = true
268+
memAuthenticator = kas
269+
}
270+
}
271+
memAuthStore := &emailCachingAuthStore{
272+
MemoryAuthStore: store.NewMemoryAuthStore(),
273+
fileStore: fsStore,
274+
}
262275
memLoginAuth := auth.NewLoginAuth(memAuthStore, memAuthenticator)
263276
memLoginAuth.WithShouldLogin(func() (bool, error) { return true, nil })
264277

@@ -555,4 +568,22 @@ var (
555568
_ store.Auth = auth.NoLoginAuth{}
556569
_ auth.AuthStore = store.FileStore{}
557570
_ auth.AuthStore = &store.MemoryAuthStore{}
571+
_ auth.AuthStore = &emailCachingAuthStore{}
558572
)
573+
574+
// emailCachingAuthStore wraps MemoryAuthStore and persists the login email
575+
// to ~/.brev/cached-email after each successful authentication.
576+
type emailCachingAuthStore struct {
577+
*store.MemoryAuthStore
578+
fileStore *store.FileStore
579+
}
580+
581+
func (e *emailCachingAuthStore) SaveAuthTokens(tokens entity.AuthTokens) error {
582+
if err := e.MemoryAuthStore.SaveAuthTokens(tokens); err != nil {
583+
return breverrors.WrapAndTrace(err)
584+
}
585+
if email := auth.GetEmailFromToken(tokens.AccessToken); email != "" {
586+
_ = e.fileStore.SaveCachedEmail(email)
587+
}
588+
return nil
589+
}

pkg/cmd/cmd_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package cmd
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/brevdev/brev-cli/pkg/entity"
9+
"github.com/brevdev/brev-cli/pkg/store"
10+
"github.com/spf13/afero"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// fakeJWT builds an unsigned JWT with the given claims (header.payload.signature).
16+
func fakeJWT(t *testing.T, claims map[string]interface{}) string {
17+
t.Helper()
18+
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
19+
payload, err := json.Marshal(claims)
20+
require.NoError(t, err)
21+
return header + "." + base64.RawURLEncoding.EncodeToString(payload) + "."
22+
}
23+
24+
func newTestFileStore(t *testing.T) *store.FileStore {
25+
t.Helper()
26+
fs := afero.NewMemMapFs()
27+
err := fs.MkdirAll("/home/testuser/.brev", 0o755)
28+
require.NoError(t, err)
29+
return store.NewBasicStore().WithFileSystem(fs).WithUserHomeDirGetter(
30+
func() (string, error) { return "/home/testuser", nil },
31+
)
32+
}
33+
34+
func TestEmailCachingAuthStore_SaveCachesEmail(t *testing.T) {
35+
fs := newTestFileStore(t)
36+
s := &emailCachingAuthStore{
37+
MemoryAuthStore: store.NewMemoryAuthStore(),
38+
fileStore: fs,
39+
}
40+
41+
token := fakeJWT(t, map[string]interface{}{"email": "user@example.com"})
42+
err := s.SaveAuthTokens(entity.AuthTokens{AccessToken: token})
43+
require.NoError(t, err)
44+
45+
cached, err := fs.GetCachedEmail()
46+
require.NoError(t, err)
47+
assert.Equal(t, "user@example.com", cached)
48+
}
49+
50+
func TestEmailCachingAuthStore_NoEmailInToken(t *testing.T) {
51+
fs := newTestFileStore(t)
52+
s := &emailCachingAuthStore{
53+
MemoryAuthStore: store.NewMemoryAuthStore(),
54+
fileStore: fs,
55+
}
56+
57+
token := fakeJWT(t, map[string]interface{}{"sub": "12345"})
58+
err := s.SaveAuthTokens(entity.AuthTokens{AccessToken: token})
59+
require.NoError(t, err)
60+
61+
cached, err := fs.GetCachedEmail()
62+
require.NoError(t, err)
63+
assert.Equal(t, "", cached)
64+
}
65+
66+
func TestEmailCachingAuthStore_EmptyAccessToken(t *testing.T) {
67+
fs := newTestFileStore(t)
68+
s := &emailCachingAuthStore{
69+
MemoryAuthStore: store.NewMemoryAuthStore(),
70+
fileStore: fs,
71+
}
72+
73+
err := s.SaveAuthTokens(entity.AuthTokens{AccessToken: ""})
74+
require.Error(t, err)
75+
}

pkg/cmd/deregister/deregister_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ type mockNetBirdManager struct {
9191
err error
9292
}
9393

94-
func (m *mockNetBirdManager) Install() error { return m.err }
95-
func (m *mockNetBirdManager) Uninstall() error { m.called = true; return m.err }
94+
func (m *mockNetBirdManager) Install() error { return m.err }
95+
func (m *mockNetBirdManager) Uninstall() error { m.called = true; return m.err }
96+
func (m *mockNetBirdManager) EnsureRunning() error { return m.err }
9697

9798
type mockNodeClientFactory struct {
9899
serverURL string

pkg/cmd/gpucreate/gpucreate.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/brevdev/brev-cli/pkg/entity"
1919
breverrors "github.com/brevdev/brev-cli/pkg/errors"
2020
"github.com/brevdev/brev-cli/pkg/featureflag"
21+
"github.com/brevdev/brev-cli/pkg/names"
2122
"github.com/brevdev/brev-cli/pkg/store"
2223
"github.com/brevdev/brev-cli/pkg/terminal"
2324
"github.com/spf13/cobra"
@@ -194,8 +195,8 @@ func NewCmdGPUCreate(t *terminal.Terminal, gpuCreateStore GPUCreateStore) *cobra
194195
}
195196
}
196197

197-
if name == "" {
198-
return breverrors.NewValidationError("name is required (as argument or --name flag)")
198+
if err := names.ValidateNodeName(name); err != nil {
199+
return breverrors.WrapAndTrace(err)
199200
}
200201

201202
if count < 1 {

pkg/cmd/register/device_registration_store.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ func (s *FileRegistrationStore) Load() (*DeviceRegistration, error) {
8585
if err := files.ReadJSON(files.AppFs, path, &reg); err != nil {
8686
return nil, breverrors.WrapAndTrace(err)
8787
}
88+
if reg.ExternalNodeID == "" || reg.OrgID == "" {
89+
return nil, breverrors.New("malformed registration")
90+
}
8891
return &reg, nil
8992
}
9093

@@ -125,6 +128,9 @@ func sudoWriteFile(path string, data []byte) error {
125128
if err := cmd.Run(); err != nil {
126129
return fmt.Errorf("sudo tee %s failed: %w", path, err)
127130
}
131+
if err := exec.Command("sudo", "chmod", "644", path).Run(); err != nil { //nolint:gosec // fixed base path
132+
return fmt.Errorf("sudo chmod %s failed: %w", path, err)
133+
}
128134
return nil
129135
}
130136

pkg/cmd/register/device_registration_store_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,48 @@ func Test_LoadRegistration_FailsWhenMissing(t *testing.T) {
144144
}
145145
}
146146

147+
func Test_LoadRegistration_RejectsMissingExternalNodeID(t *testing.T) {
148+
cleanup := setupTestFs(t)
149+
defer cleanup()
150+
151+
store := NewFileRegistrationStore()
152+
153+
reg := &DeviceRegistration{
154+
ExternalNodeID: "",
155+
DisplayName: "Test",
156+
OrgID: "org_xyz",
157+
}
158+
if err := store.Save(reg); err != nil {
159+
t.Fatalf("Save failed: %v", err)
160+
}
161+
162+
_, err := store.Load()
163+
if err == nil {
164+
t.Fatal("expected error loading registration with empty ExternalNodeID")
165+
}
166+
}
167+
168+
func Test_LoadRegistration_RejectsMissingOrgID(t *testing.T) {
169+
cleanup := setupTestFs(t)
170+
defer cleanup()
171+
172+
store := NewFileRegistrationStore()
173+
174+
reg := &DeviceRegistration{
175+
ExternalNodeID: "unode_abc",
176+
DisplayName: "Test",
177+
OrgID: "",
178+
}
179+
if err := store.Save(reg); err != nil {
180+
t.Fatalf("Save failed: %v", err)
181+
}
182+
183+
_, err := store.Load()
184+
if err == nil {
185+
t.Fatal("expected error loading registration with empty OrgID")
186+
}
187+
}
188+
147189
func Test_DeleteRegistration_FailsWhenMissing(t *testing.T) {
148190
cleanup := setupTestFs(t)
149191
defer cleanup()

pkg/cmd/register/providers.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package register
22

33
import (
4+
"fmt"
5+
"os/exec"
46
"runtime"
7+
"strings"
58

69
nodev1connect "buf.build/gen/go/brevdev/devplane/connectrpc/go/devplaneapi/v1/devplaneapiv1connect"
710

@@ -38,6 +41,33 @@ type Netbird struct{}
3841
func (Netbird) Install() error { return InstallNetbird() }
3942
func (Netbird) Uninstall() error { return UninstallNetbird() }
4043

44+
// EnsureRunning checks if the netbird systemd service is active and attempts
45+
// to start it if it is not. It also checks the netbird peer connection status
46+
// and runs "netbird up" if the peer is disconnected.
47+
func (Netbird) EnsureRunning() error {
48+
out, err := exec.Command("systemctl", "is-active", "netbird").Output() //nolint:gosec // fixed service name
49+
if err != nil || strings.TrimSpace(string(out)) != "active" {
50+
if startErr := exec.Command("sudo", "systemctl", "start", "netbird").Run(); startErr != nil { //nolint:gosec // fixed service name
51+
return fmt.Errorf("failed to start Brev tunnel service: %w", startErr)
52+
}
53+
}
54+
55+
statusOut, err := exec.Command("netbird", "status").Output() //nolint:gosec // fixed command
56+
if err != nil {
57+
// Service is running, just can't confirm peer status.
58+
return nil
59+
}
60+
61+
if netbirdManagementConnected(string(statusOut)) {
62+
return nil
63+
}
64+
65+
if upErr := exec.Command("sudo", "netbird", "up").Run(); upErr != nil { //nolint:gosec // fixed command
66+
return fmt.Errorf("failed to reconnect Brev tunnel: %w", upErr)
67+
}
68+
return nil
69+
}
70+
4171
// ShellSetupRunner runs setup scripts via shell.
4272
type ShellSetupRunner struct{}
4373

0 commit comments

Comments
 (0)