196 lines
5.6 KiB
Go
196 lines
5.6 KiB
Go
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
|
|
}
|