Skip to content

Commit 5e6c288

Browse files
authored
feat(BRE2-955): use org access instead of role attachments (#417)
* use org access instead of role attachments * uptake latest devplane * fix url
1 parent 1f27141 commit 5e6c288

10 files changed

Lines changed: 162 additions & 75 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ endif
2525
fast-build: ## go build -o brev
2626
$(call print-target)
2727
echo ${VERSION}
28-
CGO_ENABLED=1 go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
28+
$(_BUILD_PREFIX) go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
2929

3030
.PHONY: local
3131
local: ## build with env wrapper (use: make local env=dev0|dev1|dev2|stg arch=linux/amd64, or make local for defaults)

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/brevdev/brev-cli
33
go 1.25.0
44

55
require (
6-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260521231113-5bd61a2e035f.1
7-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260521231113-5bd61a2e035f.1
6+
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260626205643-49b0d20e08f1.1
7+
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260626205643-49b0d20e08f1.1
88
connectrpc.com/connect v1.20.0
99
github.com/NVIDIA/go-nvml v0.13.0-1
1010
github.com/alessio/shellescape v1.4.1

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260521231113-5bd61a2e035f.1 h1:p2gDnCmIeMzMuRNP05Jh143Q8iiSq0/oXG8eckzCkSY=
2-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260521231113-5bd61a2e035f.1/go.mod h1:CwGL+2J9G36DvGlMYW/5f+LTnGAOGJPcAw3S/Zy7lbk=
3-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260521231113-5bd61a2e035f.1 h1:NyJ55L5BmM+AOC77hUrLysVvzU4m9YO+g93YwvZS3Y4=
4-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260521231113-5bd61a2e035f.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
1+
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260626205643-49b0d20e08f1.1 h1:Qj4BTbhIF0KE5YHiJJ+SN2goGYF8dJC1l8cv69YU/Ms=
2+
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260626205643-49b0d20e08f1.1/go.mod h1:KW+lsYUmrF994Z/zj/wibrS7zhitXrYLicqR5BbVSp0=
3+
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260626205643-49b0d20e08f1.1 h1:+GNKe6qV3aRH+N/FBlH6NfqyKOxMecAtbHndj3NPZc4=
4+
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260626205643-49b0d20e08f1.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
55
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1 h1:6amhprQmCKJ4wgJ6ngkh32d9V+dQcOLUZ/SfHdOnYgo=
66
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1/go.mod h1:O+pnSHMru/naTMrm4tmpBoH3wz6PHa+R75HR7Mv8X2g=
77
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=

pkg/cmd/grantssh/grantssh.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type GrantSSHStore interface {
3030
GetOrganizationsByName(name string) ([]entity.Organization, error)
3131
ListOrganizations() ([]entity.Organization, error)
3232
GetAccessToken() (string, error)
33-
GetOrgRoleAttachments(orgID string) ([]entity.OrgRoleAttachment, error)
33+
ListOrganizationMembers(ctx context.Context, orgID string) ([]*nodev1.OrganizationMember, error)
3434
GetUserByID(userID string) (*entity.User, error)
3535
}
3636

@@ -43,8 +43,7 @@ type grantSSHDeps struct {
4343
}
4444

4545
type resolvedMember struct {
46-
user *entity.User
47-
attachment entity.OrgRoleAttachment
46+
user *entity.User
4847
}
4948

5049
func defaultGrantSSHDeps() grantSSHDeps {
@@ -161,7 +160,7 @@ func runGrantSSH(ctx context.Context, t *terminal.Terminal, s GrantSSHStore, opt
161160
return breverrors.WrapAndTrace(err)
162161
}
163162

164-
orgMembers, err := getOrgMembers(currentUser, t, s, org.ID)
163+
orgMembers, err := getOrgMembers(ctx, currentUser, t, s, org.ID)
165164
if err != nil {
166165
return err
167166
}
@@ -291,16 +290,16 @@ func findUserByIDOrEmail(members []resolvedMember, idOrEmail string) (*entity.Us
291290
return nil, fmt.Errorf("no org member found matching %q", idOrEmail)
292291
}
293292

294-
func getOrgMembers(currentUser *entity.User, t *terminal.Terminal, s GrantSSHStore, orgID string) ([]resolvedMember, error) {
295-
attachments, err := s.GetOrgRoleAttachments(orgID)
293+
func getOrgMembers(ctx context.Context, currentUser *entity.User, t *terminal.Terminal, s GrantSSHStore, orgID string) ([]resolvedMember, error) {
294+
members, err := s.ListOrganizationMembers(ctx, orgID)
296295
if err != nil {
297296
return nil, fmt.Errorf("failed to fetch org members: %w", err)
298297
}
299298

300-
var otherMembers []entity.OrgRoleAttachment
301-
for _, a := range attachments {
302-
if a.Subject != currentUser.ID {
303-
otherMembers = append(otherMembers, a)
299+
var otherMembers []*nodev1.OrganizationMember
300+
for _, member := range members {
301+
if member.GetUserId() != currentUser.ID {
302+
otherMembers = append(otherMembers, member)
304303
}
305304
}
306305

@@ -309,12 +308,12 @@ func getOrgMembers(currentUser *entity.User, t *terminal.Terminal, s GrantSSHSto
309308
}
310309
var resolved []resolvedMember
311310
for _, m := range otherMembers {
312-
memberUser, err := s.GetUserByID(m.Subject)
311+
memberUser, err := s.GetUserByID(m.GetUserId())
313312
if err != nil {
314-
t.Vprintf(" Warning: could not resolve user %s: %v\n", m.Subject, err)
313+
t.Vprintf(" Warning: could not resolve user %s: %v\n", m.GetUserId(), err)
315314
continue
316315
}
317-
resolved = append(resolved, resolvedMember{user: memberUser, attachment: m})
316+
resolved = append(resolved, resolvedMember{user: memberUser})
318317
}
319318

320319
if len(resolved) == 0 {

pkg/cmd/grantssh/grantssh_test.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ func (m *mockRegistrationStore) Exists() (bool, error) {
6262

6363
// mockGrantSSHStore satisfies GrantSSHStore.
6464
type mockGrantSSHStore struct {
65-
user *entity.User
66-
org *entity.Organization
67-
token string
68-
attachments []entity.OrgRoleAttachment
69-
users map[string]*entity.User
70-
err error
65+
user *entity.User
66+
org *entity.Organization
67+
token string
68+
members []*nodev1.OrganizationMember
69+
users map[string]*entity.User
70+
err error
7171
}
7272

7373
func (m *mockGrantSSHStore) GetCurrentUser() (*entity.User, error) {
@@ -83,8 +83,8 @@ func (m *mockGrantSSHStore) GetActiveOrganizationOrDefault() (*entity.Organizati
8383

8484
func (m *mockGrantSSHStore) GetAccessToken() (string, error) { return m.token, nil }
8585

86-
func (m *mockGrantSSHStore) GetOrgRoleAttachments(_ string) ([]entity.OrgRoleAttachment, error) {
87-
return m.attachments, nil
86+
func (m *mockGrantSSHStore) ListOrganizationMembers(_ context.Context, _ string) ([]*nodev1.OrganizationMember, error) {
87+
return m.members, nil
8888
}
8989

9090
func (m *mockGrantSSHStore) GetUserByID(userID string) (*entity.User, error) {
@@ -226,9 +226,9 @@ func Test_runGrantSSH_HappyPath(t *testing.T) {
226226
user: &entity.User{ID: "user_1", PublicKey: "ssh-ed25519 testkey"},
227227
org: &entity.Organization{ID: "org_123", Name: "TestOrg"},
228228
token: "tok",
229-
attachments: []entity.OrgRoleAttachment{
230-
{Subject: "user_1"}, // current user, should be filtered
231-
{Subject: "user_2"},
229+
members: []*nodev1.OrganizationMember{
230+
{UserId: "user_1"}, // current user, should be filtered
231+
{UserId: "user_2"},
232232
},
233233
users: map[string]*entity.User{
234234
"user_2": targetUser,
@@ -305,9 +305,9 @@ func Test_runGrantSSH_NonInteractiveWithPortID(t *testing.T) {
305305
user: &entity.User{ID: "user_1"},
306306
org: &entity.Organization{ID: "org_123", Name: "TestOrg"},
307307
token: "tok",
308-
attachments: []entity.OrgRoleAttachment{
309-
{Subject: "user_1"},
310-
{Subject: "user_2"},
308+
members: []*nodev1.OrganizationMember{
309+
{UserId: "user_1"},
310+
{UserId: "user_2"},
311311
},
312312
users: map[string]*entity.User{"user_2": targetUser},
313313
}
@@ -368,8 +368,8 @@ func Test_runGrantSSH_RPCFailure(t *testing.T) {
368368
user: &entity.User{ID: "user_1"},
369369
org: &entity.Organization{ID: "org_123", Name: "TestOrg"},
370370
token: "tok",
371-
attachments: []entity.OrgRoleAttachment{
372-
{Subject: "user_2"},
371+
members: []*nodev1.OrganizationMember{
372+
{UserId: "user_2"},
373373
},
374374
users: map[string]*entity.User{
375375
"user_2": {ID: "user_2", Name: "Alice", Email: "alice@example.com"},
@@ -406,8 +406,8 @@ func Test_runGrantSSH_NoOtherMembers(t *testing.T) {
406406
user: &entity.User{ID: "user_1"},
407407
org: &entity.Organization{ID: "org_123", Name: "TestOrg"},
408408
token: "tok",
409-
attachments: []entity.OrgRoleAttachment{
410-
{Subject: "user_1"}, // only current user, no others
409+
members: []*nodev1.OrganizationMember{
410+
{UserId: "user_1"}, // only current user, no others
411411
},
412412
users: map[string]*entity.User{},
413413
}

pkg/cmd/revokessh/revokessh.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type RevokeSSHStore interface {
2828
GetOrganizationsByName(name string) ([]entity.Organization, error)
2929
ListOrganizations() ([]entity.Organization, error)
3030
GetUserByID(userID string) (*entity.User, error)
31-
GetOrgRoleAttachments(orgID string) ([]entity.OrgRoleAttachment, error)
31+
ListOrganizationMembers(ctx context.Context, orgID string) ([]*nodev1.OrganizationMember, error)
3232
}
3333

3434
// revokeSSHDeps bundles the side-effecting dependencies of runRevokeSSH so they
@@ -183,7 +183,7 @@ func runRevokeSSH(ctx context.Context, t *terminal.Terminal, s RevokeSSHStore, o
183183
targetPortID = selectedAccess.GetPortId()
184184
portLabel = portLabelForAccess(selectedNode, selectedAccess)
185185
} else {
186-
resolvedUserID, err := resolveUserID(s, selectedOrg.ID, opts.userIDOrEmail)
186+
resolvedUserID, err := resolveUserID(ctx, s, selectedOrg.ID, opts.userIDOrEmail)
187187
if err != nil {
188188
return err
189189
}
@@ -268,7 +268,7 @@ func portLabelForAccess(node *nodev1.ExternalNode, sa *nodev1.SSHAccess) string
268268
}
269269

270270
// resolveUserID resolves idOrEmail to a Brev user ID using org members when it looks like an email.
271-
func resolveUserID(s RevokeSSHStore, orgID string, idOrEmail string) (string, error) {
271+
func resolveUserID(ctx context.Context, s RevokeSSHStore, orgID string, idOrEmail string) (string, error) {
272272
if idOrEmail == "" {
273273
return "", fmt.Errorf("user is required")
274274
}
@@ -281,12 +281,15 @@ func resolveUserID(s RevokeSSHStore, orgID string, idOrEmail string) (string, er
281281
return idOrEmail, nil
282282
}
283283

284-
attachments, err := s.GetOrgRoleAttachments(orgID)
284+
members, err := s.ListOrganizationMembers(ctx, orgID)
285285
if err != nil {
286286
return "", fmt.Errorf("failed to list org members: %w", err)
287287
}
288-
for _, a := range attachments {
289-
u, err := s.GetUserByID(a.Subject)
288+
for _, member := range members {
289+
if strings.EqualFold(member.GetDefaultEmail(), idOrEmail) {
290+
return member.GetUserId(), nil
291+
}
292+
u, err := s.GetUserByID(member.GetUserId())
290293
if err != nil {
291294
continue
292295
}

pkg/cmd/revokessh/revokessh_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ func (m *mockRegistrationStore) Exists() (bool, error) {
6060
}
6161

6262
type mockRevokeSSHStore struct {
63-
token string
64-
org *entity.Organization
65-
users map[string]*entity.User
63+
token string
64+
org *entity.Organization
65+
members []*nodev1.OrganizationMember
66+
users map[string]*entity.User
6667
}
6768

6869
func (m *mockRevokeSSHStore) GetAccessToken() (string, error) { return m.token, nil }
@@ -94,8 +95,8 @@ func (m *mockRevokeSSHStore) GetOrganizationsByName(name string) ([]entity.Organ
9495
return nil, nil
9596
}
9697

97-
func (m *mockRevokeSSHStore) GetOrgRoleAttachments(_ string) ([]entity.OrgRoleAttachment, error) {
98-
return nil, nil
98+
func (m *mockRevokeSSHStore) ListOrganizationMembers(_ context.Context, _ string) ([]*nodev1.OrganizationMember, error) {
99+
return m.members, nil
99100
}
100101

101102
// fakeNodeService implements the server side of ExternalNodeService for testing.

pkg/entity/entity.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -577,17 +577,6 @@ func (u User) GetOnboardingData() (*OnboardingData, error) {
577577
return x, nil
578578
}
579579

580-
type OrgRoleAttachment struct {
581-
Subject string `json:"subject"`
582-
Object string `json:"object"`
583-
Role OrgRoleAttachmentRole `json:"role"`
584-
}
585-
586-
type OrgRoleAttachmentRole struct {
587-
ID string `json:"id"`
588-
Actions []string `json:"actions"`
589-
}
590-
591580
type ModifyWorkspaceRequest struct {
592581
WorkspaceClass string `json:"workspaceClassId"`
593582
IsStoppable *bool `json:"isStoppable"`

pkg/store/organization.go

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
package store
22

33
import (
4+
"context"
45
"fmt"
6+
"net/http"
57
"strings"
68

9+
nodev1connect "buf.build/gen/go/brevdev/devplane/connectrpc/go/devplaneapi/v1/devplaneapiv1connect"
10+
nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
11+
"connectrpc.com/connect"
12+
713
"github.com/brevdev/brev-cli/pkg/auth"
14+
"github.com/brevdev/brev-cli/pkg/config"
815
"github.com/brevdev/brev-cli/pkg/entity"
916
breverrors "github.com/brevdev/brev-cli/pkg/errors"
1017
"github.com/brevdev/brev-cli/pkg/files"
@@ -251,28 +258,58 @@ func (s AuthHTTPStore) CreateInviteLink(organizationID string) (string, error) {
251258
return result, nil
252259
}
253260

254-
func GetDefaultOrNilOrg(orgs []entity.Organization) *entity.Organization {
255-
if len(orgs) > 0 {
256-
return &orgs[0]
257-
} else {
258-
return nil
259-
}
261+
type authHTTPStoreTransport struct {
262+
store *AuthHTTPStore
263+
base http.RoundTripper
260264
}
261265

262-
func (s AuthHTTPStore) GetOrgRoleAttachments(orgID string) ([]entity.OrgRoleAttachment, error) {
263-
var result []entity.OrgRoleAttachment
264-
res, err := s.authHTTPClient.restyClient.R().
265-
SetHeader("Content-Type", "application/json").
266-
SetResult(&result).
267-
Get(fmt.Sprintf("api/organizations/%s/role_attachments", orgID))
266+
func (t *authHTTPStoreTransport) RoundTrip(req *http.Request) (*http.Response, error) {
267+
token, err := t.store.GetAccessToken()
268268
if err != nil {
269269
return nil, breverrors.WrapAndTrace(err)
270270
}
271-
if res.IsError() {
272-
return nil, NewHTTPResponseError(res)
271+
req = req.Clone(req.Context())
272+
req.Header.Set("Authorization", "Bearer "+token)
273+
resp, err := t.base.RoundTrip(req)
274+
if err != nil {
275+
return nil, breverrors.WrapAndTrace(err)
273276
}
277+
return resp, nil
278+
}
274279

275-
return result, nil
280+
func (s *AuthHTTPStore) ListOrganizationMembers(ctx context.Context, orgID string) ([]*nodev1.OrganizationMember, error) {
281+
client := nodev1connect.NewOrganizationServiceClient(
282+
&http.Client{Transport: &authHTTPStoreTransport{store: s, base: http.DefaultTransport}},
283+
config.GlobalConfig.GetBrevPublicAPIURL(),
284+
)
285+
286+
var members []*nodev1.OrganizationMember
287+
var pageToken string
288+
for {
289+
resp, err := client.ListOrganizationMembers(ctx, connect.NewRequest(&nodev1.ListOrganizationMembersRequest{
290+
OrganizationId: orgID,
291+
PageParams: &nodev1.PageParams{
292+
PageSize: 1000,
293+
PageToken: pageToken,
294+
},
295+
}))
296+
if err != nil {
297+
return nil, breverrors.WrapAndTrace(err)
298+
}
299+
members = append(members, resp.Msg.GetItems()...)
300+
pageToken = resp.Msg.GetNextPageToken()
301+
if pageToken == "" {
302+
return members, nil
303+
}
304+
}
305+
}
306+
307+
func GetDefaultOrNilOrg(orgs []entity.Organization) *entity.Organization {
308+
if len(orgs) > 0 {
309+
return &orgs[0]
310+
} else {
311+
return nil
312+
}
276313
}
277314

278315
type RedeemCouponCodeRequest struct {

0 commit comments

Comments
 (0)