@@ -15,11 +15,12 @@ import (
1515// RepoAccessCache caches repository metadata related to lockdown checks so that
1616// multiple tools can reuse the same access information safely across goroutines.
1717type RepoAccessCache struct {
18- client * githubv4.Client
19- mu sync.Mutex
20- cache * cache2go.CacheTable
21- ttl time.Duration
22- logger * slog.Logger
18+ client * githubv4.Client
19+ mu sync.Mutex
20+ cache * cache2go.CacheTable
21+ ttl time.Duration
22+ logger * slog.Logger
23+ trustedBotLogins map [string ]struct {}
2324}
2425
2526type repoAccessCacheEntry struct {
@@ -85,6 +86,9 @@ func GetInstance(client *githubv4.Client, opts ...RepoAccessOption) *RepoAccessC
8586client :client ,
8687cache :cache2go .Cache (defaultRepoAccessCacheKey ),
8788ttl :defaultRepoAccessTTL ,
89+ trustedBotLogins :map [string ]struct {}{
90+ "copilot" : {},
91+ },
8892}
8993for _ ,opt := range opts {
9094if opt != nil {
@@ -109,13 +113,22 @@ type CacheStats struct {
109113Evictions int64
110114}
111115
116+ // IsSafeContent determines if the specified user can safely access the requested repository content.
117+ // Safe access applies when any of the following is true:
118+ // - the content was created by a trusted bot;
119+ // - the author currently has push access to the repository;
120+ // - the repository is private;
121+ // - the content was created by the viewer.
112122func (c * RepoAccessCache )IsSafeContent (ctx context.Context ,username ,owner ,repo string ) (bool ,error ) {
113123repoInfo ,err := c .getRepoAccessInfo (ctx ,username ,owner ,repo )
114124if err != nil {
115- c .logDebug ("error checking repo access info for content filtering" ,"owner" ,owner ,"repo" ,repo ,"user" ,username ,"error" ,err )
116125return false ,err
117126}
118- if repoInfo .IsPrivate || repoInfo .ViewerLogin == username {
127+
128+ c .logDebug (ctx ,fmt .Sprintf ("evaluated repo access for user %s to %s/%s for content filtering, result: hasPushAccess=%t, isPrivate=%t" ,
129+ username ,owner ,repo ,repoInfo .HasPushAccess ,repoInfo .IsPrivate ))
130+
131+ if c .isTrustedBot (username )|| repoInfo .IsPrivate || repoInfo .ViewerLogin == strings .ToLower (username ) {
119132return true ,nil
120133}
121134return repoInfo .HasPushAccess ,nil
@@ -136,30 +149,34 @@ func (c *RepoAccessCache) getRepoAccessInfo(ctx context.Context, username, owner
136149if err == nil {
137150entry := cacheItem .Data ().(* repoAccessCacheEntry )
138151if cachedHasPush ,known := entry .knownUsers [userKey ];known {
139- c .logDebug ("repo access cache hit" , "owner" , owner , "repo" , repo , "user" , username )
152+ c .logDebug (ctx , fmt . Sprintf ( "repo access cache hit for user %s to %s/%s" , username , owner , repo ) )
140153return RepoAccessInfo {
141154IsPrivate :entry .isPrivate ,
142155HasPushAccess :cachedHasPush ,
143156ViewerLogin :entry .viewerLogin ,
144157},nil
145158}
146- c .logDebug ("known users cache miss" ,"owner" ,owner ,"repo" ,repo ,"user" ,username )
159+
160+ c .logDebug (ctx ,"known users cache miss, fetching from graphql API" )
161+
147162info ,queryErr := c .queryRepoAccessInfo (ctx ,username ,owner ,repo )
148163if queryErr != nil {
149164return RepoAccessInfo {},queryErr
150165}
166+
151167entry .knownUsers [userKey ]= info .HasPushAccess
152168entry .viewerLogin = info .ViewerLogin
153169entry .isPrivate = info .IsPrivate
154170c .cache .Add (key ,c .ttl ,entry )
171+
155172return RepoAccessInfo {
156173IsPrivate :entry .isPrivate ,
157174HasPushAccess :entry .knownUsers [userKey ],
158175ViewerLogin :entry .viewerLogin ,
159176},nil
160177}
161178
162- c .logDebug ("repo access cache miss" , "owner" , owner , "repo" , repo , "user" , username )
179+ c .logDebug (ctx , fmt . Sprintf ( "repo access cache miss for user %s to %s/%s" , username , owner , repo ) )
163180
164181info ,queryErr := c .queryRepoAccessInfo (ctx ,username ,owner ,repo )
165182if queryErr != nil {
@@ -223,19 +240,35 @@ func (c *RepoAccessCache) queryRepoAccessInfo(ctx context.Context, username, own
223240}
224241}
225242
243+ c .logDebug (ctx ,fmt .Sprintf ("queried repo access info for user %s to %s/%s: isPrivate=%t, hasPushAccess=%t, viewerLogin=%s" ,
244+ username ,owner ,repo ,bool (query .Repository .IsPrivate ),hasPush ,query .Viewer .Login ))
245+
226246return RepoAccessInfo {
227247IsPrivate :bool (query .Repository .IsPrivate ),
228248HasPushAccess :hasPush ,
229249ViewerLogin :string (query .Viewer .Login ),
230250},nil
231251}
232252
233- func cacheKey (owner ,repo string )string {
234- return fmt .Sprintf ("%s/%s" ,strings .ToLower (owner ),strings .ToLower (repo ))
253+ func (c * RepoAccessCache )log (ctx context.Context ,level slog.Level ,msg string ,attrs ... slog.Attr ) {
254+ if c == nil || c .logger == nil {
255+ return
256+ }
257+ if ! c .logger .Enabled (ctx ,level ) {
258+ return
259+ }
260+ c .logger .LogAttrs (ctx ,level ,msg ,attrs ... )
235261}
236262
237- func (c * RepoAccessCache )logDebug (msg string ,args ... any ) {
238- if c != nil && c .logger != nil {
239- c .logger .Debug (msg ,args ... )
240- }
263+ func (c * RepoAccessCache )logDebug (ctx context.Context ,msg string ,attrs ... slog.Attr ) {
264+ c .log (ctx ,slog .LevelDebug ,msg ,attrs ... )
265+ }
266+
267+ func (c * RepoAccessCache )isTrustedBot (username string )bool {
268+ _ ,ok := c .trustedBotLogins [strings .ToLower (username )]
269+ return ok
270+ }
271+
272+ func cacheKey (owner ,repo string )string {
273+ return fmt .Sprintf ("%s/%s" ,strings .ToLower (owner ),strings .ToLower (repo ))
241274}