Skip to content

Commit 8b22828

Browse files
authored
Merge pull request #36 from ConductorOne/lauren/account-member-roll-all
Paginate through account members, store member id on profile
2 parents 6d43fd4 + cdcf2db commit 8b22828

File tree

4 files changed

+73
-21
lines changed

4 files changed

+73
-21
lines changed

.github/workflows/ci.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ jobs:
5252
CONNECTOR_ENTITLEMENT: 'role:1963e6e3aca5ac9a7a91609a0040ab02:member'
5353
CONNECTOR_PRINCIPAL: '9d9a62a5b834a8c9c5cf43cd234dfd4a'
5454
CONNECTOR_PRINCIPAL_TYPE: 'user'
55+
# Fake ID for failure testing
56+
FAKE_PRINCIPAL_ID: 'fake-id-that-does-not-exist-12345'
5557
steps:
5658
- name: Install Go
5759
uses: actions/setup-go@v5
@@ -74,6 +76,19 @@ jobs:
7476
run: |
7577
./baton-cloudflare
7678
baton grants --entitlement="${{ env.CONNECTOR_ENTITLEMENT }}" --output-format=json | jq --exit-status ".grants[].principal.id.resource == \"${{ env.CONNECTOR_PRINCIPAL }}\""
79+
- name: Test grant failure with fake ID
80+
run: |
81+
set +e # Don't exit on error
82+
./baton-cloudflare --grant-entitlement ${{ env.CONNECTOR_ENTITLEMENT }} --grant-principal ${{ env.FAKE_PRINCIPAL_ID }} --grant-principal-type ${{ env.CONNECTOR_PRINCIPAL_TYPE }}
83+
exit_code=$?
84+
set -e # Re-enable exit on error
85+
86+
if [ $exit_code -eq 0 ]; then
87+
echo "ERROR: Grant with fake ID should have failed but succeeded"
88+
exit 1
89+
else
90+
echo "SUCCESS: Grant with fake ID failed as expected (exit code: $exit_code)"
91+
fi
7792
- name: Revoke grants
7893
run: |
7994
./baton-cloudflare

.golangci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ linters:
7171
- durationcheck # check for two durations multiplied together
7272
- errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13.
7373
- exhaustive # check exhaustiveness of enum switch statements
74-
- exportloopref # checks for pointers to enclosing loop variables
7574
- forbidigo # Forbids identifiers
7675
- gochecknoinits # Checks that no init functions are present in Go code
7776
- goconst # Finds repeated strings that could be replaced by a constant

pkg/connector/role.go

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -228,18 +228,28 @@ func (r *roleResourceType) Grant(ctx context.Context, principal *v2.Resource, en
228228
return nil, fmt.Errorf("baton-cloudflare: only users can be granted role membership")
229229
}
230230

231-
memberId, err := getMemberId(ctx, r, userId)
231+
userTrait, err := rs.GetUserTrait(principal)
232232
if err != nil {
233-
return nil, err
233+
return nil, fmt.Errorf("baton-cloudflare: user trait not found on principal")
234+
}
235+
236+
memberId, found := rs.GetProfileStringValue(userTrait.GetProfile(), memberIdProfileKey)
237+
if !found || memberId == "" {
238+
memberId, err = getMemberId(ctx, r, userId)
239+
if err != nil {
240+
return nil, err
241+
}
234242
}
235243

236244
account, err := r.GetAccountMember(ctx, r.accountId, memberId)
237245
if err != nil {
238246
return nil, fmt.Errorf("error: %s", err.Error())
239247
}
240248

241-
roles := []cloudflare.AccountRole{{
242-
ID: roleId},
249+
roles := []cloudflare.AccountRole{
250+
{
251+
ID: roleId,
252+
},
243253
}
244254
for _, role := range account.Result.Roles {
245255
if role.ID == roleId {
@@ -248,7 +258,7 @@ func (r *roleResourceType) Grant(ctx context.Context, principal *v2.Resource, en
248258
zap.String("principal_id", principal.Id.String()),
249259
zap.String("principal_type", principal.Id.ResourceType),
250260
)
251-
return nil, fmt.Errorf("cloudflare-connector: user %s already has this role", principal.DisplayName)
261+
return annotations.New(&v2.GrantAlreadyExists{}), nil
252262
}
253263

254264
roles = append(roles, cloudflare.AccountRole{
@@ -350,18 +360,34 @@ func (r *roleResourceType) UpdateAccountMember(ctx context.Context, accountID, m
350360
}
351361

352362
func getMemberId(ctx context.Context, r *roleResourceType, userId string) (string, error) {
353-
memberUsers, _, err := r.client.AccountMembers(ctx, r.accountId, cloudflare.PaginationOptions{})
354-
if err != nil {
355-
return "", wrapError(err, "failed to list user members")
356-
}
363+
processedMemberCount := 0
364+
perPage := 50
365+
page := 1
366+
367+
for {
368+
memberUsers, resp, err := r.client.AccountMembers(ctx, r.accountId, cloudflare.PaginationOptions{
369+
Page: page,
370+
PerPage: perPage,
371+
})
372+
if err != nil {
373+
return "", wrapError(err, "failed to list user members")
374+
}
357375

358-
for _, memberUser := range memberUsers {
359-
if memberUser.User.ID == userId {
360-
return memberUser.ID, nil
376+
for _, memberUser := range memberUsers {
377+
if memberUser.User.ID == userId {
378+
return memberUser.ID, nil
379+
}
380+
}
381+
382+
processedMemberCount += perPage
383+
if processedMemberCount >= resp.Total {
384+
break
361385
}
386+
387+
page++
362388
}
363389

364-
return "", nil
390+
return "", fmt.Errorf("cloudflare-connector: account member not found for user with id: %s", userId)
365391
}
366392

367393
func (r *roleResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error) {
@@ -379,9 +405,18 @@ func (r *roleResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotat
379405

380406
userId := principal.Id.Resource
381407
roleId := entitlement.Resource.Id.Resource
382-
memberId, err := getMemberId(ctx, r, userId)
408+
409+
userTrait, err := rs.GetUserTrait(principal)
383410
if err != nil {
384-
return nil, err
411+
return nil, fmt.Errorf("baton-cloudflare: user trait not found on principal")
412+
}
413+
414+
memberId, found := rs.GetProfileStringValue(userTrait.GetProfile(), memberIdProfileKey)
415+
if !found || memberId == "" {
416+
memberId, err = getMemberId(ctx, r, userId)
417+
if err != nil {
418+
return nil, err
419+
}
385420
}
386421

387422
account, err := r.GetAccountMember(ctx, r.accountId, memberId)
@@ -407,7 +442,7 @@ func (r *roleResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotat
407442
zap.String("principal_id", principal.Id.String()),
408443
zap.String("principal_type", principal.Id.ResourceType),
409444
)
410-
return nil, fmt.Errorf("cloudflare-connector: user %s does not have this role", principal.DisplayName)
445+
return annotations.New(&v2.GrantAlreadyRevoked{}), nil
411446
}
412447

413448
member, err := r.UpdateAccountMember(ctx, r.accountId, memberId, cloudflare.AccountMember{

pkg/connector/user.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
rs "github.com/conductorone/baton-sdk/pkg/types/resource"
1212
)
1313

14+
const memberIdProfileKey = "member_id"
15+
1416
type UserResourceType struct {
1517
resourceType *v2.ResourceType
1618
client *cloudflare.API
@@ -26,10 +28,11 @@ func userResource(member cloudflare.AccountMember) (*v2.Resource, error) {
2628
firstName := user.FirstName
2729
lastName := user.LastName
2830
profile := map[string]interface{}{
29-
"login": user.Email,
30-
"first_name": firstName,
31-
"last_name": lastName,
32-
"email": user.Email,
31+
"login": user.Email,
32+
"first_name": firstName,
33+
"last_name": lastName,
34+
"email": user.Email,
35+
memberIdProfileKey: member.ID,
3336
}
3437

3538
userTraits := []rs.UserTraitOption{

0 commit comments

Comments
 (0)