-
Notifications
You must be signed in to change notification settings - Fork 116
feat(gitlab): Cache results acl project membership #2280
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
base: main
Are you sure you want to change the base?
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -27,12 +27,30 @@ func (v *Provider) IsAllowedOwnersFile(_ context.Context, event *info.Event) (bo | |||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
func (v *Provider) checkMembership(ctx context.Context, event *info.Event, userid int) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// Initialize cache lazily | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if v.memberCache == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
v.memberCache = map[int]bool{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
if allowed, ok := v.memberCache[userid]; ok { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return allowed | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
member, _, err := v.Client().ProjectMembers.GetInheritedProjectMember(v.targetProjectID, userid) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if err == nil && member.ID != 0 && member.ID == userid { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// If the API call fails, fall back without caching the result so a | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// transient failure can be retried on the next invocation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
isAllowed, _ := v.IsAllowedOwnersFile(ctx, event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return isAllowed | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
if member.ID != 0 && member.ID == userid { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
v.memberCache[userid] = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
isAllowed, _ := v.IsAllowedOwnersFile(ctx, event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
v.memberCache[userid] = isAllowed | ||||||||||||||||||||||||||||||||||||||||||||||||||||
chmouel marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+40
to
+53
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. Small change but simplifies a bit
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
return isAllowed | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,6 +1,8 @@ | ||||||
package gitlab | ||||||
|
||||||
import ( | ||||||
"fmt" | ||||||
"net/http" | ||||||
"testing" | ||||||
|
||||||
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info" | ||||||
|
@@ -130,3 +132,107 @@ func TestIsAllowed(t *testing.T) { | |||||
}) | ||||||
} | ||||||
} | ||||||
|
||||||
func TestMembershipCaching(t *testing.T) { | ||||||
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. Can you also add a test to check the caching on
|
||||||
ctx, _ := rtesting.SetupFakeContext(t) | ||||||
|
||||||
v := &Provider{ | ||||||
targetProjectID: 3030, | ||||||
userID: 4242, | ||||||
} | ||||||
|
||||||
client, mux, tearDown := thelp.Setup(t) | ||||||
defer tearDown() | ||||||
v.gitlabClient = client | ||||||
|
||||||
// Count how many times the membership API is hit. | ||||||
var calls int | ||||||
thelp.MuxAllowUserIDCounting(mux, v.targetProjectID, v.userID, &calls) | ||||||
|
||||||
ev := &info.Event{Sender: "someone", PullRequestNumber: 1} | ||||||
|
||||||
// First call should hit the API once and cache the result. | ||||||
allowed, err := v.IsAllowed(ctx, ev) | ||||||
if err != nil { | ||||||
t.Fatalf("unexpected error: %v", err) | ||||||
} | ||||||
if !allowed { | ||||||
t.Fatalf("expected allowed on first membership check") | ||||||
} | ||||||
if calls < 1 { | ||||||
t.Fatalf("expected at least 1 membership API call, got %d", calls) | ||||||
} | ||||||
|
||||||
// Second call should use the cache and not hit the API again. | ||||||
allowed, err = v.IsAllowed(ctx, ev) | ||||||
if err != nil { | ||||||
t.Fatalf("unexpected error: %v", err) | ||||||
} | ||||||
if !allowed { | ||||||
t.Fatalf("expected allowed on cached membership check") | ||||||
} | ||||||
if calls != 1 { | ||||||
t.Fatalf("expected cached result with no extra API call, got %d calls", calls) | ||||||
} | ||||||
} | ||||||
|
||||||
func TestMembershipAPIFailureDoesNotCacheFalse(t *testing.T) { | ||||||
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. Test name is slightly misleading; if the API successfully returns false, it will/should be cached (assuming the owners file check also returns false).
Suggested change
|
||||||
ctx, _ := rtesting.SetupFakeContext(t) | ||||||
|
||||||
v := &Provider{ | ||||||
targetProjectID: 3030, | ||||||
userID: 4242, | ||||||
} | ||||||
|
||||||
client, mux, tearDown := thelp.Setup(t) | ||||||
defer tearDown() | ||||||
v.gitlabClient = client | ||||||
|
||||||
ev := &info.Event{Sender: "someone"} | ||||||
|
||||||
var ( | ||||||
calls int | ||||||
success bool | ||||||
) | ||||||
path := fmt.Sprintf("/projects/%d/members/all/%d", v.targetProjectID, v.userID) | ||||||
mux.HandleFunc(path, func(rw http.ResponseWriter, _ *http.Request) { | ||||||
calls++ | ||||||
if !success { | ||||||
rw.WriteHeader(http.StatusInternalServerError) | ||||||
_, _ = rw.Write([]byte(`{}`)) | ||||||
return | ||||||
} | ||||||
_, err := fmt.Fprintf(rw, `{"id": %d}`, v.userID) | ||||||
if err != nil { | ||||||
t.Fatalf("failed to write response: %v", err) | ||||||
} | ||||||
}) | ||||||
|
||||||
thelp.MuxDiscussionsNoteEmpty(mux, v.targetProjectID, ev.PullRequestNumber) | ||||||
|
||||||
allowed, err := v.IsAllowed(ctx, ev) | ||||||
if err != nil { | ||||||
t.Fatalf("unexpected error on failure path: %v", err) | ||||||
} | ||||||
if allowed { | ||||||
t.Fatalf("expected not allowed when membership API fails and no fallback grants access") | ||||||
} | ||||||
if calls < 1 { | ||||||
t.Fatalf("expected at least 1 membership API call, got %d", calls) | ||||||
} | ||||||
initialCallCount := calls | ||||||
|
||||||
// Make the next API call succeed; the provider should retry because the previous failure wasn't cached. | ||||||
success = true | ||||||
|
||||||
allowed, err = v.IsAllowed(ctx, ev) | ||||||
if err != nil { | ||||||
t.Fatalf("unexpected error on retry path: %v", err) | ||||||
} | ||||||
if !allowed { | ||||||
t.Fatalf("expected allowed when membership API succeeds on retry") | ||||||
} | ||||||
if calls <= initialCallCount { | ||||||
t.Fatalf("expected membership API to be called again after retry, got %d total calls (initial %d)", calls, initialCallCount) | ||||||
} | ||||||
} |
Uh oh!
There was an error while loading. Please reload this page.
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.
@chmouel like in Konflux, what if event comes User
A
is member of RepositoryR
and it is cached but same user does something for RepositoryB
and there UserA
is not member or an approved user, but due to cacheA
will be allowed. wdyt?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.
shouldn't it be mapping to repository URL
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.
that should not happen because it works by event not across repository tho,