chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,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
|
||||
}
|
||||
Reference in New Issue
Block a user