-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Expand file tree
/
Copy pathscopes.go
More file actions
195 lines (162 loc) · 5.6 KB
/
scopes.go
File metadata and controls
195 lines (162 loc) · 5.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package scopes
import (
"slices"
"sort"
)
// Scope represents a GitHub OAuth scope.
// These constants define all OAuth scopes used by the GitHub MCP server tools.
// See https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps
type Scope string
const (
// NoScope indicates no scope is required (public access).
NoScope Scope = ""
// Repo grants full control of private repositories
Repo Scope = "repo"
// PublicRepo grants access to public repositories
PublicRepo Scope = "public_repo"
// ReadOrg grants read-only access to organization membership, teams, and projects
ReadOrg Scope = "read:org"
// WriteOrg grants write access to organization membership and teams
WriteOrg Scope = "write:org"
// AdminOrg grants full control of organizations and teams
AdminOrg Scope = "admin:org"
// Gist grants write access to gists
Gist Scope = "gist"
// Notifications grants access to notifications
Notifications Scope = "notifications"
// ReadProject grants read-only access to projects
ReadProject Scope = "read:project"
// Project grants full control of projects
Project Scope = "project"
// SecurityEvents grants read and write access to security events
SecurityEvents Scope = "security_events"
// User grants read/write access to profile info
User Scope = "user"
// ReadUser grants read-only access to profile info
ReadUser Scope = "read:user"
// UserEmail grants read access to user email addresses
UserEmail Scope = "user:email"
// ReadPackages grants read access to packages
ReadPackages Scope = "read:packages"
// WritePackages grants write access to packages
WritePackages Scope = "write:packages"
)
// ScopeHierarchy defines parent-child relationships between scopes.
// A parent scope implicitly grants access to all child scopes.
// For example, "repo" grants access to "public_repo" and "security_events".
var ScopeHierarchy = map[Scope][]Scope{
Repo: {PublicRepo, SecurityEvents},
AdminOrg: {WriteOrg, ReadOrg},
WriteOrg: {ReadOrg},
Project: {ReadProject},
WritePackages: {ReadPackages},
User: {ReadUser, UserEmail},
}
// ScopeSet represents a set of OAuth scopes.
type ScopeSet map[Scope]bool
// NewScopeSet creates a new ScopeSet from the given scopes.
func NewScopeSet(scopes ...Scope) ScopeSet {
set := make(ScopeSet)
for _, scope := range scopes {
set[scope] = true
}
return set
}
// ToSlice converts a ScopeSet to a slice of Scope values.
func (s ScopeSet) ToSlice() []Scope {
scopes := make([]Scope, 0, len(s))
for scope := range s {
scopes = append(scopes, scope)
}
// Sort for deterministic output
slices.Sort(scopes)
return scopes
}
// ToStringSlice converts a ScopeSet to a slice of string values.
// The returned slice is sorted for deterministic output.
func (s ScopeSet) ToStringSlice() []string {
scopes := make([]string, 0, len(s))
for scope := range s {
scopes = append(scopes, string(scope))
}
sort.Strings(scopes)
return scopes
}
// ToStringSlice converts a slice of Scopes to a slice of strings.
func ToStringSlice(scopes ...Scope) []string {
result := make([]string, len(scopes))
for i, scope := range scopes {
result[i] = string(scope)
}
return result
}
// ExpandScopes takes a list of required scopes and returns all accepted scopes
// including parent scopes from the hierarchy.
// For example, if "public_repo" is required, "repo" is also accepted since
// having the "repo" scope grants access to "public_repo".
// The returned slice is sorted for deterministic output.
func ExpandScopes(required ...Scope) []string {
if len(required) == 0 {
return nil
}
accepted := make(map[string]bool)
// Add required scopes
for _, scope := range required {
accepted[string(scope)] = true
}
// Add parent scopes that grant access to required scopes
for parent, children := range ScopeHierarchy {
for _, child := range children {
if accepted[string(child)] {
accepted[string(parent)] = true
}
}
}
// Convert to slice and sort for deterministic output
result := make([]string, 0, len(accepted))
for scope := range accepted {
result = append(result, scope)
}
sort.Strings(result)
return result
}
// expandScopeSet returns a set of all scopes granted by the given scopes,
// including child scopes from the hierarchy.
// For example, if "repo" is provided, the result includes "repo", "public_repo",
// and "security_events" since "repo" grants access to those child scopes.
func expandScopeSet(scopes []string) map[string]bool {
expanded := make(map[string]bool, len(scopes))
for _, scope := range scopes {
expanded[scope] = true
// Add child scopes granted by this scope
if children, ok := ScopeHierarchy[Scope(scope)]; ok {
for _, child := range children {
expanded[string(child)] = true
}
}
}
return expanded
}
// HasRequiredScopes checks if tokenScopes satisfy the acceptedScopes requirement.
// A tool's acceptedScopes includes both the required scopes AND parent scopes
// that implicitly grant the required permissions (via ExpandScopes).
//
// For PAT filtering: if ANY of the acceptedScopes are granted by the token
// (directly or via scope hierarchy), the tool should be visible.
//
// Returns true if the tool should be visible to the token holder.
func HasRequiredScopes(tokenScopes []string, acceptedScopes []string) bool {
// No scopes required = always allowed
if len(acceptedScopes) == 0 {
return true
}
// Expand token scopes to include child scopes they grant
grantedScopes := expandScopeSet(tokenScopes)
// Check if any accepted scope is granted by the token
for _, accepted := range acceptedScopes {
if grantedScopes[accepted] {
return true
}
}
return false
}