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
+ }