-
Notifications
You must be signed in to change notification settings - Fork 0
Guard scope grant emission behind WillSyncResourceType #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,11 @@ const ( | |
|
|
||
| type teammateBuilder struct { | ||
| client SendGridClient | ||
| // skipScopeResourceType reports whether scope is excluded from the sync | ||
| // filter. Only the scope emission below is gated: teammates have their own | ||
| // entitlements and subuser grants, so the resource-type-level skip | ||
| // annotations would suppress real data and are deliberately not used here. | ||
| skipScopeResourceType bool | ||
| } | ||
|
|
||
| func (u *teammateBuilder) ResourceType(ctx context.Context) *v2.ResourceType { | ||
|
|
@@ -241,8 +246,9 @@ func (u *teammateBuilder) Grants(ctx context.Context, resource *v2.Resource, opt | |
| rv = append(rv, grants...) | ||
| } | ||
|
|
||
| // Scope grants — only on the first (and only) page to avoid duplicate API calls. | ||
| if opts.PageToken.Token == "" { | ||
| // Scope grants — only on the first (and only) page to avoid duplicate API | ||
| // calls, and only when scope is in the sync filter. | ||
| if opts.PageToken.Token == "" && !u.skipScopeResourceType { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: the scope target is now gated, but the other cross-type emission in this same method — |
||
| specificTeammate, err := u.client.GetSpecificTeammate(ctx, sgclient.Username(username), sgclient.OnBehalfOf(onBehalfOf)) | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("baton-sendgrid: failed to get teammate %s: %w", username, err) | ||
|
|
@@ -293,9 +299,10 @@ func (u *teammateBuilder) Delete(ctx context.Context, resourceId *v2.ResourceId, | |
| return nil, nil | ||
| } | ||
|
|
||
| func newTeammateBuilder(client SendGridClient) *teammateBuilder { | ||
| func newTeammateBuilder(client SendGridClient, skipScopeResourceType bool) *teammateBuilder { | ||
| return &teammateBuilder{ | ||
| client: client, | ||
| client: client, | ||
| skipScopeResourceType: skipScopeResourceType, | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -195,7 +195,7 @@ func TestTeammateBuilder_List_RootTeammates(t *testing.T) { | |
| }, | ||
| } | ||
|
|
||
| tb := newTeammateBuilder(client) | ||
| tb := newTeammateBuilder(client, false) | ||
| resources := drainTeammateList(t, tb, nil, nil) | ||
|
|
||
| require.Len(t, resources, 2) | ||
|
|
@@ -222,7 +222,7 @@ func TestTeammateBuilder_List_SubuserTeammates(t *testing.T) { | |
| }, | ||
| } | ||
|
|
||
| tb := newTeammateBuilder(client) | ||
| tb := newTeammateBuilder(client, false) | ||
|
|
||
| sub1ResourceID, err := rs.NewResourceID(subuserResourceType, 1) | ||
| require.NoError(t, err) | ||
|
|
@@ -261,7 +261,7 @@ func TestTeammateBuilder_List_TeammateRestrictedToMultipleSubusers(t *testing.T) | |
| }, | ||
| } | ||
|
|
||
| tb := newTeammateBuilder(client) | ||
| tb := newTeammateBuilder(client, false) | ||
| session := newFakeSessionStore() | ||
|
|
||
| sub1ResourceID, err := rs.NewResourceID(subuserResourceType, 1) | ||
|
|
@@ -298,7 +298,7 @@ func TestTeammateBuilder_Grants_SubuserAccessForbidden(t *testing.T) { | |
| resource, err := teammateResource(&models.Teammate{Username: "local-1", Email: "local-1@example.com"}, sub1ResourceID, "sub1") | ||
| require.NoError(t, err) | ||
|
|
||
| tb := newTeammateBuilder(client) | ||
| tb := newTeammateBuilder(client, false) | ||
| grants, results, err := tb.Grants(context.Background(), resource, rs.SyncOpAttrs{PageToken: pagination.Token{}}) | ||
|
|
||
| require.NoError(t, err, "a 403 from subuser_access must not abort Grants for a subuser-only teammate") | ||
|
|
@@ -325,8 +325,43 @@ func TestTeammateBuilder_Grants_SubuserAccessOtherErrorPropagates(t *testing.T) | |
| resource, err := teammateResource(&models.Teammate{Username: "local-1", Email: "local-1@example.com"}, sub1ResourceID, "sub1") | ||
| require.NoError(t, err) | ||
|
|
||
| tb := newTeammateBuilder(client) | ||
| tb := newTeammateBuilder(client, false) | ||
| _, _, err = tb.Grants(context.Background(), resource, rs.SyncOpAttrs{PageToken: pagination.Token{}}) | ||
|
|
||
| require.Error(t, err, "only PermissionDenied should be tolerated, other errors must still propagate") | ||
| } | ||
|
|
||
| // scopeCallRecorder records whether the scope lookup was attempted. | ||
| type scopeCallRecorder struct { | ||
| fakeSendGridClient | ||
| called bool | ||
| } | ||
|
|
||
| func (f *scopeCallRecorder) GetSpecificTeammate(_ context.Context, _ sgclient.Username, _ sgclient.OnBehalfOf) (*models.TeammateScope, error) { | ||
| f.called = true | ||
| return &models.TeammateScope{Teammate: models.Teammate{Username: "u1"}, Scopes: []string{"mail.send"}}, nil | ||
| } | ||
|
|
||
| // Scope grants are cross-type. When scope is excluded from the sync filter the | ||
| // connector must not even make the per-teammate scope lookup. Subuser grants | ||
| // are unaffected: teammates own those. | ||
| func TestTeammateBuilder_Grants_SkipScopeResourceType(t *testing.T) { | ||
| ctx := context.Background() | ||
| res, err := teammateResource(&models.Teammate{Username: "u1", Email: "u1@example.com"}, nil, "") | ||
| require.NoError(t, err) | ||
|
|
||
| filtered := &scopeCallRecorder{} | ||
| tb := newTeammateBuilder(filtered, true) | ||
| grants, _, err := tb.Grants(ctx, res, rs.SyncOpAttrs{}) | ||
| require.NoError(t, err) | ||
| require.False(t, filtered.called, "scope lookup must be skipped when scope is filtered out") | ||
| for _, g := range grants { | ||
| require.NotEqual(t, scopeResourceType.Id, g.GetEntitlement().GetResource().GetId().GetResourceType()) | ||
| } | ||
|
|
||
| inScope := &scopeCallRecorder{} | ||
| tb = newTeammateBuilder(inScope, false) | ||
| _, _, err = tb.Grants(ctx, res, rs.SyncOpAttrs{}) | ||
| require.NoError(t, err) | ||
| require.True(t, inScope.called, "scope lookup must run when scope is in the sync filter") | ||
|
Comment on lines
+362
to
+366
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: the positive branch discards |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Suggestion:
Newnow takes two adjacent, same-typedboolparams (ignoreSubusers,skipScopeResourceType). Callers can silently swap them with no compile error, and this list will keep growing as more resource types get gated. Consider a small options struct (or variadicOptfuncs) so each flag is named at the call site.