From 4216ab2e417df2e9ed6c33492489b2e482a1421b Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Tue, 7 Jul 2026 12:50:10 -0700 Subject: [PATCH 1/2] feat: migrate identity stores to dev-plane --- pkg/store/devplane.go | 198 +++++++++++++++++++++++++++ pkg/store/devplane_migration_test.go | 142 +++++++++++++++++++ pkg/store/http.go | 1 + pkg/store/organization.go | 76 +++------- pkg/store/organization_test.go | 50 +++---- pkg/store/user.go | 43 ++---- pkg/store/user_test.go | 25 +--- pkg/store/workspace.go | 8 +- 8 files changed, 405 insertions(+), 138 deletions(-) create mode 100644 pkg/store/devplane.go create mode 100644 pkg/store/devplane_migration_test.go diff --git a/pkg/store/devplane.go b/pkg/store/devplane.go new file mode 100644 index 000000000..405cd9221 --- /dev/null +++ b/pkg/store/devplane.go @@ -0,0 +1,198 @@ +package store + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + nodev1connect "buf.build/gen/go/brevdev/devplane/connectrpc/go/devplaneapi/v1/devplaneapiv1connect" + nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1" + "connectrpc.com/connect" + + "github.com/brevdev/brev-cli/pkg/config" + "github.com/brevdev/brev-cli/pkg/entity" + breverrors "github.com/brevdev/brev-cli/pkg/errors" +) + +type devPlaneUserService interface { + GetCurrentUser(context.Context) (*nodev1.User, string, error) + GetCurrentUserKeys(context.Context) (*entity.UserKeys, error) + GetUser(context.Context, string) (*nodev1.User, error) + SetUserBlocked(context.Context, string, bool) (*nodev1.User, error) +} + +type devPlaneOrganizationService interface { + ListOrganizations(context.Context, []string) ([]*nodev1.Organization, error) + GetOrganization(context.Context, string) (*nodev1.Organization, error) +} + +type devPlaneServices struct { + user devPlaneUserService + organization devPlaneOrganizationService +} + +type authHTTPStoreTransport struct { + store *AuthHTTPStore + base http.RoundTripper +} + +func (t *authHTTPStoreTransport) RoundTrip(req *http.Request) (*http.Response, error) { + token, err := t.store.GetAccessToken() + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + req = req.Clone(req.Context()) + req.Header.Set("Authorization", "Bearer "+token) + resp, err := t.base.RoundTrip(req) + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + return resp, nil +} + +func (s *AuthHTTPStore) devPlaneHTTPClient() *http.Client { + return &http.Client{Transport: &authHTTPStoreTransport{store: s, base: http.DefaultTransport}} +} + +func (s *AuthHTTPStore) withDevPlaneServices(services *devPlaneServices) *AuthHTTPStore { + copy := *s + copy.devPlane = services + return © +} + +func (s *AuthHTTPStore) devPlaneServiceClients() *devPlaneServices { + if s.devPlane != nil { + return s.devPlane + } + httpClient := s.devPlaneHTTPClient() + baseURL := config.GlobalConfig.GetBrevPublicAPIURL() + return &devPlaneServices{ + user: &generatedDevPlaneUserService{client: nodev1connect.NewUserServiceClient(httpClient, baseURL)}, + organization: &generatedDevPlaneOrganizationService{ + client: nodev1connect.NewOrganizationServiceClient(httpClient, baseURL), + }, + } +} + +type generatedDevPlaneUserService struct { + client nodev1connect.UserServiceClient +} + +func (s *generatedDevPlaneUserService) GetCurrentUser(ctx context.Context) (*nodev1.User, string, error) { + response, err := s.client.GetCurrentUser(ctx, connect.NewRequest(&nodev1.GetCurrentUserRequest{})) + if err != nil { + return nil, "", breverrors.WrapAndTrace(err) + } + // The public SSH-key response field is added by Set 1. Keep the local + // contract ready and populate it after the landed Buf revision is available. + return response.Msg.GetUser(), "", nil +} + +func (s *generatedDevPlaneUserService) GetCurrentUserKeys(context.Context) (*entity.UserKeys, error) { + return nil, fmt.Errorf("GetCurrentUserSSHKeys requires the published Set 1 Buf revision") +} + +func (s *generatedDevPlaneUserService) GetUser(ctx context.Context, userID string) (*nodev1.User, error) { + response, err := s.client.GetUser(ctx, connect.NewRequest(&nodev1.GetUserRequest{ + UserId: userID, + AttachedDataOptions: &nodev1.UserAttachedDataOptions{ + Emails: true, + ExternalIdentities: true, + OrganizationAccesses: true, + }, + })) + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + return response.Msg.GetUser(), nil +} + +func (s *generatedDevPlaneUserService) SetUserBlocked(context.Context, string, bool) (*nodev1.User, error) { + return nil, fmt.Errorf("SetUserBlocked requires the published Set 1 Buf revision") +} + +type generatedDevPlaneOrganizationService struct { + client nodev1connect.OrganizationServiceClient +} + +func (s *generatedDevPlaneOrganizationService) ListOrganizations(ctx context.Context, ids []string) ([]*nodev1.Organization, error) { + organizations := []*nodev1.Organization{} + pageToken := "" + for { + response, err := s.client.ListOrganization(ctx, connect.NewRequest(&nodev1.ListOrganizationRequest{ + PageParams: &nodev1.PageParams{PageSize: 1000, PageToken: pageToken}, + Options: &nodev1.ListOrganizationOptions{Ids: ids}, + })) + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + organizations = append(organizations, response.Msg.GetItems()...) + pageToken = response.Msg.GetNextPageToken() + if pageToken == "" { + return organizations, nil + } + } +} + +func (s *generatedDevPlaneOrganizationService) GetOrganization(ctx context.Context, id string) (*nodev1.Organization, error) { + response, err := s.client.GetOrganization(ctx, connect.NewRequest(&nodev1.GetOrganizationRequest{OrganizationId: id})) + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + return response.Msg.GetOrganization(), nil +} + +func mapDevPlaneUser(user *nodev1.User, publicSSHKey string) (*entity.User, error) { + if user == nil { + return nil, fmt.Errorf("dev-plane returned no user") + } + metadata := struct { + BaseWorkspaceRepo string `json:"baseWorkspaceRepo"` + UserSetupExecPath string `json:"userSetupExecPath"` + WorkspacePassword string `json:"workspacePassword"` + GlobalUserType entity.GlobalUserType `json:"globalUserType"` + IDEConfig entity.IDEConfig `json:"ideConfig"` + OnboardingData map[string]interface{} `json:"onboardingData"` + }{} + if user.GetMetadata() != nil { + data, err := json.Marshal(user.GetMetadata().AsMap()) + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + if err := json.Unmarshal(data, &metadata); err != nil { + return nil, breverrors.WrapAndTrace(err) + } + } + var externalIdentities []*entity.ExternalIdentity + for _, identity := range user.GetExternalIdentities() { + externalIdentities = append(externalIdentities, &entity.ExternalIdentity{ + IdentityID: identity.GetIdentityId(), + Provider: identity.GetProvider(), + ExternalID: identity.GetExternalId(), + }) + } + return &entity.User{ + ID: user.GetUserId(), + PublicKey: publicSSHKey, + Username: user.GetUsername(), + Name: user.GetDisplayName(), + Email: user.GetDefaultEmail(), + ExternalIdentities: externalIdentities, + WorkspacePassword: metadata.WorkspacePassword, + BaseWorkspaceRepo: metadata.BaseWorkspaceRepo, + GlobalUserType: metadata.GlobalUserType, + IdeConfig: metadata.IDEConfig, + OnboardingData: metadata.OnboardingData, + }, nil +} + +func mapDevPlaneOrganization(organization *nodev1.Organization) (*entity.Organization, error) { + if organization == nil { + return nil, fmt.Errorf("dev-plane returned no organization") + } + return &entity.Organization{ + ID: organization.GetOrganizationId(), + Name: organization.GetDisplayName(), + }, nil +} diff --git a/pkg/store/devplane_migration_test.go b/pkg/store/devplane_migration_test.go new file mode 100644 index 000000000..b2b2ae914 --- /dev/null +++ b/pkg/store/devplane_migration_test.go @@ -0,0 +1,142 @@ +package store + +import ( + "context" + "testing" + + nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1" + "github.com/brevdev/brev-cli/pkg/entity" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +type mockDevPlaneUserService struct { + currentUser *nodev1.User + publicSSHKey string + keys *entity.UserKeys + getUser *nodev1.User + gotUserID string + gotBlockedUserID string + gotBlocked bool +} + +func (m *mockDevPlaneUserService) GetCurrentUser(context.Context) (*nodev1.User, string, error) { + return m.currentUser, m.publicSSHKey, nil +} + +func (m *mockDevPlaneUserService) GetCurrentUserKeys(context.Context) (*entity.UserKeys, error) { + return m.keys, nil +} + +func (m *mockDevPlaneUserService) GetUser(_ context.Context, userID string) (*nodev1.User, error) { + m.gotUserID = userID + return m.getUser, nil +} + +func (m *mockDevPlaneUserService) SetUserBlocked(_ context.Context, userID string, blocked bool) (*nodev1.User, error) { + m.gotBlockedUserID = userID + m.gotBlocked = blocked + return m.getUser, nil +} + +type mockDevPlaneOrganizationService struct { + organizations []*nodev1.Organization + organization *nodev1.Organization + gotIDs []string + gotID string +} + +func (m *mockDevPlaneOrganizationService) ListOrganizations(_ context.Context, ids []string) ([]*nodev1.Organization, error) { + m.gotIDs = ids + return m.organizations, nil +} + +func (m *mockDevPlaneOrganizationService) GetOrganization(_ context.Context, id string) (*nodev1.Organization, error) { + m.gotID = id + return m.organization, nil +} + +func TestIdentityStoresUseDevPlaneServices(t *testing.T) { + metadata, err := structpb.NewStruct(map[string]any{ + "baseWorkspaceRepo": "github.com/alice/base", + "globalUserType": "Admin", + "onboardingData": map[string]any{ + "usedCLI": true, + }, + }) + require.NoError(t, err) + apiUser := &nodev1.User{ + UserId: "user-1", + Username: "alice", + DisplayName: "Alice", + DefaultEmail: "alice@example.com", + PlatformRole: "user", + Metadata: metadata, + ExternalIdentities: []*nodev1.ExternalIdentity{{ + IdentityId: "identity-1", + ExternalId: "external-1", + Provider: "kas", + }}, + } + users := &mockDevPlaneUserService{ + currentUser: apiUser, + publicSSHKey: "ssh-rsa public", + keys: &entity.UserKeys{PublicKey: "public", PrivateKey: "private"}, + getUser: apiUser, + } + store := MakeMockAuthHTTPStore().withDevPlaneServices(&devPlaneServices{user: users}) + + current, err := store.GetCurrentUser() + require.NoError(t, err) + require.Equal(t, "user-1", current.ID) + require.Equal(t, "Alice", current.Name) + require.Equal(t, "alice@example.com", current.Email) + require.Equal(t, "ssh-rsa public", current.PublicKey) + require.Equal(t, "github.com/alice/base", current.BaseWorkspaceRepo) + require.Equal(t, entity.Admin, current.GlobalUserType) + require.Equal(t, true, current.OnboardingData["usedCLI"]) + + keys, err := store.GetCurrentUserKeys() + require.NoError(t, err) + require.Equal(t, "private", keys.PrivateKey) + + target, err := store.GetUserByID("user-2") + require.NoError(t, err) + require.Equal(t, "user-1", target.ID) + require.Equal(t, "user-2", users.gotUserID) + + require.NoError(t, store.BanUser("user-2")) + require.Equal(t, "user-2", users.gotBlockedUserID) + require.True(t, users.gotBlocked) +} + +func TestOrganizationStoresUseCurrentUserAccessesAndDevPlane(t *testing.T) { + users := &mockDevPlaneUserService{currentUser: &nodev1.User{ + UserId: "user-1", + OrganizationAccesses: []*nodev1.OrganizationAccess{ + {OrganizationId: "org-1"}, + {OrganizationId: "org-2"}, + }, + }} + organizations := &mockDevPlaneOrganizationService{ + organizations: []*nodev1.Organization{ + {OrganizationId: "org-1", DisplayName: "One"}, + {OrganizationId: "org-2", DisplayName: "Two"}, + }, + organization: &nodev1.Organization{OrganizationId: "org-2", DisplayName: "Two"}, + } + store := MakeMockAuthHTTPStore().withDevPlaneServices(&devPlaneServices{ + user: users, + organization: organizations, + }) + + listed, err := store.GetOrganizations(nil) + require.NoError(t, err) + require.Equal(t, []entity.Organization{{ID: "org-1", Name: "One"}, {ID: "org-2", Name: "Two"}}, listed) + require.Equal(t, []string{"org-1", "org-2"}, organizations.gotIDs) + + organization, err := store.GetOrganization("org-2") + require.NoError(t, err) + require.Equal(t, &entity.Organization{ID: "org-2", Name: "Two"}, organization) + require.Equal(t, "org-2", organizations.gotID) +} diff --git a/pkg/store/http.go b/pkg/store/http.go index 60884f810..57392460c 100644 --- a/pkg/store/http.go +++ b/pkg/store/http.go @@ -50,6 +50,7 @@ type AuthHTTPStore struct { NoAuthHTTPStore authHTTPClient *AuthHTTPClient isRefreshTokenHandlerSet bool + devPlane *devPlaneServices BasicStore } diff --git a/pkg/store/organization.go b/pkg/store/organization.go index bc02aedb4..9d66134da 100644 --- a/pkg/store/organization.go +++ b/pkg/store/organization.go @@ -2,8 +2,6 @@ package store import ( "context" - "fmt" - "net/http" "strings" nodev1connect "buf.build/gen/go/brevdev/devplane/connectrpc/go/devplaneapi/v1/devplaneapiv1connect" @@ -154,10 +152,26 @@ type GetOrganizationsOptions struct { } func (s AuthHTTPStore) GetOrganizations(options *GetOrganizationsOptions) ([]entity.Organization, error) { - orgs, err := s.getOrganizations() + apiUser, _, err := s.devPlaneServiceClients().user.GetCurrentUser(context.Background()) if err != nil { return nil, breverrors.WrapAndTrace(err) } + organizationIDs := make([]string, 0, len(apiUser.GetOrganizationAccesses())) + for _, access := range apiUser.GetOrganizationAccesses() { + organizationIDs = append(organizationIDs, access.GetOrganizationId()) + } + apiOrganizations, err := s.devPlaneServiceClients().organization.ListOrganizations(context.Background(), organizationIDs) + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + orgs := make([]entity.Organization, 0, len(apiOrganizations)) + for _, apiOrganization := range apiOrganizations { + organization, err := mapDevPlaneOrganization(apiOrganization) + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + orgs = append(orgs, *organization) + } if options == nil || options.Name == "" { return orgs, nil @@ -182,43 +196,12 @@ func (s AuthHTTPStore) ListOrganizations() ([]entity.Organization, error) { return s.GetOrganizations(nil) } -func (s AuthHTTPStore) getOrganizations() ([]entity.Organization, error) { - var result []entity.Organization - res, err := s.authHTTPClient.restyClient.R(). - SetHeader("Content-Type", "application/json"). - SetResult(&result). - Get(orgPath) - if err != nil { - return nil, breverrors.WrapAndTrace(err) - } - if res.IsError() { - return nil, NewHTTPResponseError(res) - } - - return result, nil -} - -var ( - orgParamName = "organizationID" - orgIDPathPattern = "api/organizations/%s" - orgIDPath = fmt.Sprintf(orgIDPathPattern, fmt.Sprintf("{%s}", orgParamName)) -) - func (s AuthHTTPStore) GetOrganization(organizationID string) (*entity.Organization, error) { - var result entity.Organization - res, err := s.authHTTPClient.restyClient.R(). - SetHeader("Content-Type", "application/json"). - SetResult(&result). - SetPathParam(orgIDParamName, organizationID). - Get(orgIDPath) + apiOrganization, err := s.devPlaneServiceClients().organization.GetOrganization(context.Background(), organizationID) if err != nil { return nil, breverrors.WrapAndTrace(err) } - if res.IsError() { - return nil, NewHTTPResponseError(res) - } - - return &result, nil + return mapDevPlaneOrganization(apiOrganization) } type CreateOrganizationRequest struct { @@ -258,28 +241,9 @@ func (s AuthHTTPStore) CreateInviteLink(organizationID string) (string, error) { return result, nil } -type authHTTPStoreTransport struct { - store *AuthHTTPStore - base http.RoundTripper -} - -func (t *authHTTPStoreTransport) RoundTrip(req *http.Request) (*http.Response, error) { - token, err := t.store.GetAccessToken() - if err != nil { - return nil, breverrors.WrapAndTrace(err) - } - req = req.Clone(req.Context()) - req.Header.Set("Authorization", "Bearer "+token) - resp, err := t.base.RoundTrip(req) - if err != nil { - return nil, breverrors.WrapAndTrace(err) - } - return resp, nil -} - func (s *AuthHTTPStore) ListOrganizationMembers(ctx context.Context, orgID string) ([]*nodev1.OrganizationMember, error) { client := nodev1connect.NewOrganizationServiceClient( - &http.Client{Transport: &authHTTPStoreTransport{store: s, base: http.DefaultTransport}}, + s.devPlaneHTTPClient(), config.GlobalConfig.GetBrevPublicAPIURL(), ) diff --git a/pkg/store/organization_test.go b/pkg/store/organization_test.go index ec7f0cebb..1547fdc84 100644 --- a/pkg/store/organization_test.go +++ b/pkg/store/organization_test.go @@ -45,20 +45,19 @@ func TestGetActiveOrganization(t *testing.T) { } func TestGetOrganizations(t *testing.T) { - fs := MakeMockAuthHTTPStore() - httpmock.ActivateNonDefault(fs.authHTTPClient.restyClient.GetClient()) - defer httpmock.DeactivateAndReset() - expected := []entity.Organization{{ ID: "1", Name: "test", }} - res, err := httpmock.NewJsonResponder(200, expected) - if !assert.Nil(t, err) { - return - } - url := fmt.Sprintf("%s/%s", fs.authHTTPClient.restyClient.BaseURL, orgPath) - httpmock.RegisterResponder("GET", url, res) + fs := MakeMockAuthHTTPStore().withDevPlaneServices(&devPlaneServices{ + user: &mockDevPlaneUserService{currentUser: &nodev1.User{ + OrganizationAccesses: []*nodev1.OrganizationAccess{{OrganizationId: "1"}}, + }}, + organization: &mockDevPlaneOrganizationService{organizations: []*nodev1.Organization{{ + OrganizationId: "1", + DisplayName: "test", + }}}, + }) org, err := fs.GetOrganizations(nil) if !assert.Nil(t, err) { @@ -114,8 +113,7 @@ func TestGetActiveOrganization_APIKeyUsesCredentialOrg(t *testing.T) { apiKey := authpkg.BrevAPIKeyPrefix + "test-key" fileStore, _, _ := newAuthTokenTestStore(t) s := fileStore.WithAuthHTTPClient(NewAuthHTTPClient(MockAuth{token: &apiKey}, "https://api.test")) - httpmock.ActivateNonDefault(s.authHTTPClient.restyClient.GetClient()) - defer httpmock.DeactivateAndReset() + s = s.withDevPlaneServices(&devPlaneServices{organization: &mockDevPlaneOrganizationService{}}) require.NoError(t, s.SaveAuthTokens(entity.AuthTokens{ APIKey: apiKey, @@ -138,8 +136,9 @@ func TestGetActiveOrganization_APIKeyUsesCredentialOrgNameWhenAvailable(t *testi apiKey := authpkg.BrevAPIKeyPrefix + "test-key" fileStore, _, _ := newAuthTokenTestStore(t) s := fileStore.WithAuthHTTPClient(NewAuthHTTPClient(MockAuth{token: &apiKey}, "https://api.test")) - httpmock.ActivateNonDefault(s.authHTTPClient.restyClient.GetClient()) - defer httpmock.DeactivateAndReset() + s = s.withDevPlaneServices(&devPlaneServices{organization: &mockDevPlaneOrganizationService{ + organization: &nodev1.Organization{OrganizationId: "org-api-key", DisplayName: "friendly-org"}, + }}) require.NoError(t, s.SaveAuthTokens(entity.AuthTokens{ APIKey: apiKey, @@ -149,10 +148,6 @@ func TestGetActiveOrganization_APIKeyUsesCredentialOrgNameWhenAvailable(t *testi ID: "org-api-key", Name: "friendly-org", } - res, err := httpmock.NewJsonResponder(200, expected) - require.NoError(t, err) - url := fmt.Sprintf("%s/%s", s.authHTTPClient.restyClient.BaseURL, fmt.Sprintf(orgIDPathPattern, "org-api-key")) - httpmock.RegisterResponder("GET", url, res) org, err := s.GetActiveOrganizationOrDefault() @@ -163,10 +158,6 @@ func TestGetActiveOrganization_APIKeyUsesCredentialOrgNameWhenAvailable(t *testi } func TestGetOrganizationsFiltersNameCaseInsensitive(t *testing.T) { - fs := MakeMockAuthHTTPStore() - httpmock.ActivateNonDefault(fs.authHTTPClient.restyClient.GetClient()) - defer httpmock.DeactivateAndReset() - expected := []entity.Organization{{ ID: "1", Name: "TEST", @@ -178,12 +169,15 @@ func TestGetOrganizationsFiltersNameCaseInsensitive(t *testing.T) { Name: "Other", }, } - res, err := httpmock.NewJsonResponder(200, orgs) - if !assert.Nil(t, err) { - return - } - url := fmt.Sprintf("%s/%s", fs.authHTTPClient.restyClient.BaseURL, orgPath) - httpmock.RegisterResponder("GET", url, res) + fs := MakeMockAuthHTTPStore().withDevPlaneServices(&devPlaneServices{ + user: &mockDevPlaneUserService{currentUser: &nodev1.User{ + OrganizationAccesses: []*nodev1.OrganizationAccess{{OrganizationId: "1"}, {OrganizationId: "2"}}, + }}, + organization: &mockDevPlaneOrganizationService{organizations: []*nodev1.Organization{ + {OrganizationId: orgs[0].ID, DisplayName: orgs[0].Name}, + {OrganizationId: orgs[1].ID, DisplayName: orgs[1].Name}, + }}, + }) org, err := fs.GetOrganizations(&GetOrganizationsOptions{Name: "test"}) if !assert.Nil(t, err) { diff --git a/pkg/store/user.go b/pkg/store/user.go index fe44be9fa..3bd9d6493 100644 --- a/pkg/store/user.go +++ b/pkg/store/user.go @@ -1,6 +1,7 @@ package store import ( + "context" "fmt" "github.com/brevdev/brev-cli/pkg/auth" @@ -8,19 +9,14 @@ import ( breverrors "github.com/brevdev/brev-cli/pkg/errors" ) -var mePath = "api/me" - func (s AuthHTTPStore) GetCurrentUser() (*entity.User, error) { - var result entity.User - res, err := s.authHTTPClient.restyClient.R(). - SetHeader("Content-Type", "application/json"). - SetResult(&result). - Get(mePath) + apiUser, publicSSHKey, err := s.devPlaneServiceClients().user.GetCurrentUser(context.Background()) if err != nil { return nil, breverrors.WrapAndTrace(err) } - if res.IsError() { - return nil, NewHTTPResponseError(res) + result, err := mapDevPlaneUser(apiUser, publicSSHKey) + if err != nil { + return nil, breverrors.WrapAndTrace(err) } // Check if user has multiple identities and is using Auth0 @@ -45,7 +41,7 @@ func (s AuthHTTPStore) GetCurrentUser() (*entity.User, error) { Email: result.Email, }) - return &result, nil + return result, nil } func (s AuthHTTPStore) GetCurrentUserID() (string, error) { @@ -63,21 +59,12 @@ func (s AuthHTTPStore) GetCurrentUserID() (string, error) { return user.ID, nil } -var userKeysPath = fmt.Sprintf("%s/keys", mePath) - func (s AuthHTTPStore) GetCurrentUserKeys() (*entity.UserKeys, error) { - var result entity.UserKeys - res, err := s.authHTTPClient.restyClient.R(). - SetHeader("Content-Type", "application/json"). - SetResult(&result). - Get(userKeysPath) + result, err := s.devPlaneServiceClients().user.GetCurrentUserKeys(context.Background()) if err != nil { return nil, breverrors.WrapAndTrace(err) } - if res.IsError() { - return nil, NewHTTPResponseError(res) - } - return &result, nil + return result, nil } var usersPath = "api/users" @@ -130,19 +117,15 @@ var usersIDPathPattern = fmt.Sprintf("%s/%s", usersPath, "%s") // usersIDPath = fmt.Sprintf(usersIDPathPattern, fmt.Sprintf("{%s}", userIDParamStr)) func (s AuthHTTPStore) GetUserByID(userID string) (*entity.User, error) { - var result entity.User - res, err := s.authHTTPClient.restyClient.R(). - SetHeader("Content-Type", "application/json"). - SetResult(&result). - Get(fmt.Sprintf(usersIDPathPattern, userID)) + apiUser, err := s.devPlaneServiceClients().user.GetUser(context.Background(), userID) if err != nil { return nil, breverrors.WrapAndTrace(err) } - if res.IsError() { - return nil, NewHTTPResponseError(res) + result, err := mapDevPlaneUser(apiUser, "") + if err != nil { + return nil, breverrors.WrapAndTrace(err) } - - return &result, nil + return result, nil } func (s AuthHTTPStore) GetUsers(queryParams map[string]string) ([]entity.User, error) { diff --git a/pkg/store/user_test.go b/pkg/store/user_test.go index 8aa738772..9c00bf21e 100644 --- a/pkg/store/user_test.go +++ b/pkg/store/user_test.go @@ -4,24 +4,19 @@ import ( "fmt" "testing" + nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1" "github.com/brevdev/brev-cli/pkg/entity" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" ) func TestGetCurrentUser(t *testing.T) { - s := MakeMockAuthHTTPStore() - httpmock.ActivateNonDefault(s.authHTTPClient.restyClient.GetClient()) - expected := &entity.User{ ID: "1", } - res, err := httpmock.NewJsonResponder(200, expected) - if !assert.Nil(t, err) { - return - } - url := fmt.Sprintf("%s/%s", s.authHTTPClient.restyClient.BaseURL, mePath) - httpmock.RegisterResponder("GET", url, res) + s := MakeMockAuthHTTPStore().withDevPlaneServices(&devPlaneServices{ + user: &mockDevPlaneUserService{currentUser: &nodev1.User{UserId: expected.ID}}, + }) u, err := s.GetCurrentUser() if !assert.Nil(t, err) { @@ -37,19 +32,13 @@ func TestGetCurrentUser(t *testing.T) { } func TestGetCurrentUserKeys(t *testing.T) { - s := MakeMockAuthHTTPStore() - httpmock.ActivateNonDefault(s.authHTTPClient.restyClient.GetClient()) - expected := &entity.UserKeys{ PrivateKey: "priv", PublicKey: "pub", } - res, err := httpmock.NewJsonResponder(200, expected) - if !assert.Nil(t, err) { - return - } - url := fmt.Sprintf("%s/%s", s.authHTTPClient.restyClient.BaseURL, userKeysPath) - httpmock.RegisterResponder("GET", url, res) + s := MakeMockAuthHTTPStore().withDevPlaneServices(&devPlaneServices{ + user: &mockDevPlaneUserService{keys: expected}, + }) u, err := s.GetCurrentUserKeys() if !assert.Nil(t, err) { diff --git a/pkg/store/workspace.go b/pkg/store/workspace.go index 0b7f44b13..b2ca876c5 100644 --- a/pkg/store/workspace.go +++ b/pkg/store/workspace.go @@ -1,6 +1,7 @@ package store import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -517,15 +518,10 @@ func (s AuthHTTPStore) DeleteWorkspace(workspaceID string) (*entity.Workspace, e } func (s AuthHTTPStore) BanUser(userID string) error { - res, err := s.authHTTPClient.restyClient.R(). - SetHeader("Content-Type", "application/json"). - Put(fmt.Sprintf("/api/users/%s/block", userID)) + _, err := s.devPlaneServiceClients().user.SetUserBlocked(context.Background(), userID, true) if err != nil { return breverrors.WrapAndTrace(err) } - if res.IsError() { - return NewHTTPResponseError(res) - } return nil } From 3378b3e4752299ba3370326e8335c7b3b10e2985 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Fri, 10 Jul 2026 14:59:27 -0700 Subject: [PATCH 2/2] remove ban --- pkg/cmd/cmd.go | 2 - pkg/cmd/fu/fu.go | 107 --------------------------- pkg/cmd/fu/fu_test.go | 1 - pkg/store/devplane.go | 5 -- pkg/store/devplane_migration_test.go | 22 ++---- pkg/store/workspace.go | 9 --- 6 files changed, 5 insertions(+), 141 deletions(-) delete mode 100644 pkg/cmd/fu/fu.go delete mode 100644 pkg/cmd/fu/fu_test.go diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index acf1bc67c..475de44ef 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -19,7 +19,6 @@ import ( "github.com/brevdev/brev-cli/pkg/cmd/envvars" "github.com/brevdev/brev-cli/pkg/cmd/exec" "github.com/brevdev/brev-cli/pkg/cmd/feedback" - "github.com/brevdev/brev-cli/pkg/cmd/fu" "github.com/brevdev/brev-cli/pkg/cmd/gpucreate" "github.com/brevdev/brev-cli/pkg/cmd/gpusearch" "github.com/brevdev/brev-cli/pkg/cmd/grantssh" @@ -293,7 +292,6 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor cmd.AddCommand(clipboard.ForwardPort(t, loginCmdStore)) cmd.AddCommand(envvars.NewCmdEnvVars(t, loginCmdStore)) cmd.AddCommand(connect.NewCmdConnect(t, noLoginCmdStore)) - cmd.AddCommand(fu.NewCmdFu(t, loginCmdStore, noLoginCmdStore)) } else { _ = 0 // noop } diff --git a/pkg/cmd/fu/fu.go b/pkg/cmd/fu/fu.go deleted file mode 100644 index 74091c328..000000000 --- a/pkg/cmd/fu/fu.go +++ /dev/null @@ -1,107 +0,0 @@ -package fu - -import ( - "fmt" - "time" - - "github.com/brevdev/brev-cli/pkg/cmd/completions" - "github.com/brevdev/brev-cli/pkg/entity" - breverrors "github.com/brevdev/brev-cli/pkg/errors" - "github.com/brevdev/brev-cli/pkg/store" - "github.com/brevdev/brev-cli/pkg/terminal" - "github.com/hashicorp/go-multierror" - "github.com/spf13/cobra" - stripmd "github.com/writeas/go-strip-markdown" -) - -var ( - fuLong string - fuExample = "brev fu " -) - -type FuStore interface { - completions.CompletionStore - DeleteWorkspace(workspaceID string) (*entity.Workspace, error) - GetWorkspaces(organizationID string, options *store.GetWorkspacesOptions) ([]entity.Workspace, error) - BanUser(userID string) error - GetWorkspaceByNameOrID(orgID string, nameOrID string) ([]entity.Workspace, error) - GetAllOrgsAsAdmin(userID string) ([]entity.Organization, error) -} - -func NewCmdFu(t *terminal.Terminal, loginFuStore FuStore, noLoginFuStore FuStore) *cobra.Command { - cmd := &cobra.Command{ - Annotations: map[string]string{"hidden": ""}, - Use: "fu", - DisableFlagsInUseLine: true, - Short: "Fetch all workspaces for a user and delete them", - Long: stripmd.Strip(fuLong), - Example: fuExample, - ValidArgsFunction: completions.GetAllWorkspaceNameCompletionHandler(noLoginFuStore, t), - RunE: func(cmd *cobra.Command, args []string) error { - var allError error - for _, userID := range args { - err := fuUser(userID, t, loginFuStore) - if err != nil { - allError = multierror.Append(allError, err) - } - } - if allError != nil { - return breverrors.WrapAndTrace(allError) - } - return nil - }, - } - - return cmd -} - -func fuUser(userID string, t *terminal.Terminal, fuStore FuStore) error { - orgs, err := fuStore.GetAllOrgsAsAdmin(userID) - if err != nil { - return breverrors.WrapAndTrace(err) - } - - var allWorkspaces []entity.Workspace - for _, org := range orgs { - workspaces, errr := fuStore.GetWorkspaces(org.ID, nil) - if errr != nil { - return breverrors.WrapAndTrace(errr) - } - allWorkspaces = append(allWorkspaces, workspaces...) - } - - s := t.NewSpinner() - s.Suffix = " Fetching workspaces for user " + userID - s.Start() - time.Sleep(5 * time.Second) - s.Stop() - - confirm := terminal.PromptGetInput(terminal.PromptContent{ - Label: fmt.Sprintf("Are you sure you want to delete all %d workspaces for user %s? (y/n)", len(allWorkspaces), userID), - ErrorMsg: "You must confirm to proceed.", - AllowEmpty: false, - }) - if confirm != "y" { - return nil - } - - for _, workspace := range allWorkspaces { - _, err2 := fuStore.DeleteWorkspace(workspace.ID) - if err2 != nil { - t.Vprintf("%s", t.Red("Failed to delete workspace with ID: %s\n", workspace.ID)) - t.Vprintf("%s", t.Red("Error: %s\n", err.Error())) - continue - } - t.Vprintf("✅ Deleted workspace %s\n", workspace.Name) - } - - err = fuStore.BanUser(userID) - if err != nil { - t.Vprintf("%s", t.Red("Failed to ban user with ID: %s\n", userID)) - t.Vprintf("%s", t.Red("Error: %s\n", err.Error())) - } - t.Vprint("\n") - t.Vprintf("🖕 Banned user %s\n", userID) - - return nil -} diff --git a/pkg/cmd/fu/fu_test.go b/pkg/cmd/fu/fu_test.go deleted file mode 100644 index 5818a26b1..000000000 --- a/pkg/cmd/fu/fu_test.go +++ /dev/null @@ -1 +0,0 @@ -package fu diff --git a/pkg/store/devplane.go b/pkg/store/devplane.go index 405cd9221..29afbd8aa 100644 --- a/pkg/store/devplane.go +++ b/pkg/store/devplane.go @@ -19,7 +19,6 @@ type devPlaneUserService interface { GetCurrentUser(context.Context) (*nodev1.User, string, error) GetCurrentUserKeys(context.Context) (*entity.UserKeys, error) GetUser(context.Context, string) (*nodev1.User, error) - SetUserBlocked(context.Context, string, bool) (*nodev1.User, error) } type devPlaneOrganizationService interface { @@ -108,10 +107,6 @@ func (s *generatedDevPlaneUserService) GetUser(ctx context.Context, userID strin return response.Msg.GetUser(), nil } -func (s *generatedDevPlaneUserService) SetUserBlocked(context.Context, string, bool) (*nodev1.User, error) { - return nil, fmt.Errorf("SetUserBlocked requires the published Set 1 Buf revision") -} - type generatedDevPlaneOrganizationService struct { client nodev1connect.OrganizationServiceClient } diff --git a/pkg/store/devplane_migration_test.go b/pkg/store/devplane_migration_test.go index b2b2ae914..4afb04c9f 100644 --- a/pkg/store/devplane_migration_test.go +++ b/pkg/store/devplane_migration_test.go @@ -11,13 +11,11 @@ import ( ) type mockDevPlaneUserService struct { - currentUser *nodev1.User - publicSSHKey string - keys *entity.UserKeys - getUser *nodev1.User - gotUserID string - gotBlockedUserID string - gotBlocked bool + currentUser *nodev1.User + publicSSHKey string + keys *entity.UserKeys + getUser *nodev1.User + gotUserID string } func (m *mockDevPlaneUserService) GetCurrentUser(context.Context) (*nodev1.User, string, error) { @@ -33,12 +31,6 @@ func (m *mockDevPlaneUserService) GetUser(_ context.Context, userID string) (*no return m.getUser, nil } -func (m *mockDevPlaneUserService) SetUserBlocked(_ context.Context, userID string, blocked bool) (*nodev1.User, error) { - m.gotBlockedUserID = userID - m.gotBlocked = blocked - return m.getUser, nil -} - type mockDevPlaneOrganizationService struct { organizations []*nodev1.Organization organization *nodev1.Organization @@ -104,10 +96,6 @@ func TestIdentityStoresUseDevPlaneServices(t *testing.T) { require.NoError(t, err) require.Equal(t, "user-1", target.ID) require.Equal(t, "user-2", users.gotUserID) - - require.NoError(t, store.BanUser("user-2")) - require.Equal(t, "user-2", users.gotBlockedUserID) - require.True(t, users.gotBlocked) } func TestOrganizationStoresUseCurrentUserAccessesAndDevPlane(t *testing.T) { diff --git a/pkg/store/workspace.go b/pkg/store/workspace.go index b2ca876c5..a1f273689 100644 --- a/pkg/store/workspace.go +++ b/pkg/store/workspace.go @@ -1,7 +1,6 @@ package store import ( - "context" "encoding/json" "fmt" "io/ioutil" @@ -517,14 +516,6 @@ func (s AuthHTTPStore) DeleteWorkspace(workspaceID string) (*entity.Workspace, e return &result, nil } -func (s AuthHTTPStore) BanUser(userID string) error { - _, err := s.devPlaneServiceClients().user.SetUserBlocked(context.Background(), userID, true) - if err != nil { - return breverrors.WrapAndTrace(err) - } - return nil -} - func (s AuthHTTPStore) GetAllOrgsAsAdmin(userID string) ([]entity.Organization, error) { var result []entity.Organization res, err := s.authHTTPClient.restyClient.R().