1+ package github
2+
3+ import (
4+ "context"
5+ "fmt"
6+ "strings"
7+ "sync"
8+
9+ "github.com/shurcooL/githubv4"
10+ )
11+
12+ // contextKey is a private type used for context keys
13+ type contextKey int
14+
15+ const (
16+ // ContentFilterKey is the key used to access content filter settings from context
17+ contentFilterKey contextKey = iota
18+ )
19+
20+ // ContentFilterSettings holds the configuration for content filtering
21+ type ContentFilterSettings struct {
22+ // Enabled indicates if content filtering is enabled
23+ Enabled bool
24+ // TrustedRepo is the repository in format "owner/repo" that is used to check permissions
25+ TrustedRepo string
26+ // OwnerRepo is the parsed owner and repo from TrustedRepo
27+ OwnerRepo OwnerRepo
28+ // IsPrivate indicates if the trusted repo is private
29+ IsPrivate bool
30+ // TrustedUsers is a map of users who have been verified to have push access
31+ TrustedUsers map [string ]bool
32+ // mu protects the TrustedUsers map
33+ mu sync.RWMutex
34+ }
35+
36+ // OwnerRepo holds the parsed owner and repo from a string in the format "owner/repo"
37+ type OwnerRepo struct {
38+ Owner string
39+ Repo string
40+ }
41+
42+ // ParseOwnerRepo parses a string in the format "owner/repo" into an OwnerRepo struct
43+ func ParseOwnerRepo (s string ) (OwnerRepo , error ) {
44+ parts := strings .Split (s , "/" )
45+ if len (parts ) != 2 || parts [0 ] == "" || parts [1 ] == "" {
46+ return OwnerRepo {}, fmt .Errorf ("invalid format for owner/repo: %s" , s )
47+ }
48+ return OwnerRepo {Owner : parts [0 ], Repo : parts [1 ]}, nil
49+ }
50+
51+ // GetContentFilterFromContext retrieves the content filter settings from the context
52+ func GetContentFilterFromContext (ctx context.Context ) (* ContentFilterSettings , bool ) {
53+ if ctx == nil {
54+ return nil , false
55+ }
56+ settings , ok := ctx .Value (contentFilterKey ).(* ContentFilterSettings )
57+ return settings , ok
58+ }
59+
60+ // InitContentFilter initializes the content filter in the context
61+ func InitContentFilter (ctx context.Context , trustedRepo string , getGQLClient GetGQLClientFn ) (context.Context , error ) {
62+ if trustedRepo == "" {
63+ // Content filtering is not enabled
64+ return ctx , nil
65+ }
66+
67+ ownerRepo , err := ParseOwnerRepo (trustedRepo )
68+ if err != nil {
69+ return ctx , err
70+ }
71+
72+ settings := & ContentFilterSettings {
73+ Enabled : true ,
74+ TrustedRepo : trustedRepo ,
75+ OwnerRepo : ownerRepo ,
76+ TrustedUsers : map [string ]bool {},
77+ }
78+
79+ // Check if the repository is private, if so, disable content filtering
80+ isPrivate , err := IsRepoPrivate (ctx , settings .OwnerRepo , getGQLClient )
81+ if err != nil {
82+ return ctx , fmt .Errorf ("failed to check repository visibility: %w" , err )
83+ }
84+ settings .IsPrivate = isPrivate
85+
86+ return context .WithValue (ctx , contentFilterKey , settings ), nil
87+ }
88+
89+ // IsRepoPrivate checks if a repository is private using GraphQL
90+ func IsRepoPrivate (ctx context.Context , ownerRepo OwnerRepo , getGQLClient GetGQLClientFn ) (bool , error ) {
91+ client , err := getGQLClient (ctx )
92+ if err != nil {
93+ return false , fmt .Errorf ("failed to get GraphQL client: %w" , err )
94+ }
95+
96+ var query struct {
97+ Repository struct {
98+ IsPrivate githubv4.Boolean
99+ } `graphql:"repository(owner: $owner, name: $name)"`
100+ }
101+
102+ variables := map [string ]interface {}{
103+ "owner" : githubv4 .String (ownerRepo .Owner ),
104+ "name" : githubv4 .String (ownerRepo .Repo ),
105+ }
106+
107+ err = client .Query (ctx , & query , variables )
108+ if err != nil {
109+ return false , fmt .Errorf ("failed to query repository visibility: %w" , err )
110+ }
111+
112+ return bool (query .Repository .IsPrivate ), nil
113+ }
114+
115+ // HasPushAccess checks if a user has push access to the trusted repository
116+ func HasPushAccess (ctx context.Context , username string , getGQLClient GetGQLClientFn ) (bool , error ) {
117+ settings , ok := GetContentFilterFromContext (ctx )
118+ if ! ok || ! settings .Enabled || settings .IsPrivate {
119+ // If filtering is not enabled or repo is private, all users are trusted
120+ return true , nil
121+ }
122+
123+ // Check cache first
124+ settings .mu .RLock ()
125+ trusted , found := settings .TrustedUsers [username ]
126+ settings .mu .RUnlock ()
127+ if found {
128+ return trusted , nil
129+ }
130+
131+ // Query GitHub API for permission
132+ client , err := getGQLClient (ctx )
133+ if err != nil {
134+ return false , fmt .Errorf ("failed to get GraphQL client: %w" , err )
135+ }
136+
137+ var query struct {
138+ Repository struct {
139+ Collaborators struct {
140+ Edges []struct {
141+ Permission githubv4.String
142+ Node struct {
143+ Login githubv4.String
144+ }
145+ }
146+ } `graphql:"collaborators(query: $username, first: 1)"`
147+ } `graphql:"repository(owner: $owner, name: $name)"`
148+ }
149+
150+ variables := map [string ]interface {}{
151+ "owner" : githubv4 .String (settings .OwnerRepo .Owner ),
152+ "name" : githubv4 .String (settings .OwnerRepo .Repo ),
153+ "username" : githubv4 .String (username ),
154+ }
155+
156+ err = client .Query (ctx , & query , variables )
157+ if err != nil {
158+ return false , fmt .Errorf ("failed to query user permissions: %w" , err )
159+ }
160+
161+ // Check if the user has push access
162+ hasPush := false
163+ for _ , edge := range query .Repository .Collaborators .Edges {
164+ login := string (edge .Node .Login )
165+ if strings .EqualFold (login , username ) {
166+ permission := string (edge .Permission )
167+ // WRITE, ADMIN, and MAINTAIN permissions have push access
168+ hasPush = permission == "WRITE" || permission == "ADMIN" || permission == "MAINTAIN"
169+ break
170+ }
171+ }
172+
173+ // Cache the result
174+ settings .mu .Lock ()
175+ settings .TrustedUsers [username ] = hasPush
176+ settings .mu .Unlock ()
177+
178+ return hasPush , nil
179+ }
180+
181+ // ShouldIncludeContent checks if content from a user should be included
182+ func ShouldIncludeContent (ctx context.Context , username string , getGQLClient GetGQLClientFn ) bool {
183+ settings , ok := GetContentFilterFromContext (ctx )
184+ if ! ok || ! settings .Enabled || settings .IsPrivate {
185+ // If filtering is not enabled or repo is private, include all content
186+ return true
187+ }
188+
189+ // Check if user has push access
190+ hasPush , err := HasPushAccess (ctx , username , getGQLClient )
191+ if err != nil {
192+ // If there's an error checking permissions, default to not including the content for safety
193+ return false
194+ }
195+ return hasPush
196+ }
0 commit comments