feat(32.1-02): expand OAuth MCP scopes from the published catalog
- Publish the compiled member-token catalog as pact.MemberTokenScopeCatalog - Imply declared names only when the MCP subject has a ClientID - Reject a bad Implies and the reserved names backend and offline_access
This commit is contained in:
@@ -15,23 +15,29 @@ import (
|
||||
// a built-in (read, write, ai) or one a plugin declared through
|
||||
// pact.HasFrontendTokenScopes. Label and Description are phrase keys
|
||||
// (containing "::") or literal text; PluginID is the declaring plugin, empty
|
||||
// for a built-in.
|
||||
// for a built-in. Implies is empty on a built-in. On a declared scope it is
|
||||
// empty, read, or write: an OAuth MCP grant of that stock scope also
|
||||
// satisfies the declared name. The name is not stored on the grant.
|
||||
type APITokenScope struct {
|
||||
Name string
|
||||
Label string
|
||||
Description string
|
||||
PluginID string
|
||||
BuiltIn bool
|
||||
Implies string
|
||||
}
|
||||
|
||||
// APITokenScopeCatalog is the compiled list of member token scopes of one
|
||||
// application: the built-ins first, then the declared scopes in activation
|
||||
// order. Boot publishes it; APITokenScopesFor reads it.
|
||||
// order. Boot publishes it, and the same value as pact.MemberTokenScopeCatalog.
|
||||
// APITokenScopesFor reads the concrete catalog.
|
||||
type APITokenScopeCatalog struct {
|
||||
scopes []APITokenScope
|
||||
byName map[string]int
|
||||
}
|
||||
|
||||
var _ pact.MemberTokenScopeCatalog = (*APITokenScopeCatalog)(nil)
|
||||
|
||||
// Scopes returns a copy of the catalog, built-ins first.
|
||||
func (c *APITokenScopeCatalog) Scopes() []APITokenScope {
|
||||
if c == nil {
|
||||
@@ -49,6 +55,23 @@ func (c *APITokenScopeCatalog) Allowed(name string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// MemberTokenScopes returns the compiled list as pact scopes, built-ins
|
||||
// first, then declared scopes. Implied names are not a separate entry: they
|
||||
// are the Implies field of the declared scope.
|
||||
func (c *APITokenScopeCatalog) MemberTokenScopes() []pact.FrontendTokenScope {
|
||||
scopes := c.Scopes()
|
||||
out := make([]pact.FrontendTokenScope, 0, len(scopes))
|
||||
for _, scope := range scopes {
|
||||
out = append(out, pact.FrontendTokenScope{
|
||||
Name: scope.Name,
|
||||
Label: scope.Label,
|
||||
Description: scope.Description,
|
||||
Implies: scope.Implies,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// builtInAPITokenScopeKeys are the label and description phrase keys of
|
||||
// the built-in scopes, in AllowedAPITokenScopes order.
|
||||
var builtInAPITokenScopeKeys = map[string][2]string{
|
||||
@@ -78,12 +101,14 @@ func newAPITokenScopeCatalog(scopes []APITokenScope) *APITokenScopeCatalog {
|
||||
// CompileAPITokenScopes builds the member token scope catalog from the
|
||||
// built-ins and every plugin that implements pact.HasFrontendTokenScopes, in
|
||||
// the order given (party.Activated). A declared name is trimmed and must
|
||||
// pass pact.ValidTokenScopeName, must not be a built-in (read, write, ai)
|
||||
// and must not repeat a name another plugin (or the same one) declared; its
|
||||
// label and description must not be empty. Any violation is an error, which
|
||||
// fails boot. Phrase keys are checked against the translator by Boot. Every
|
||||
// name of a catalog that compiles is added to the process-wide set the
|
||||
// legacy MintAPIToken accepts.
|
||||
// pass pact.ValidTokenScopeName, must not be a built-in (read, write, ai),
|
||||
// must not be the reserved name backend or offline_access, and must not
|
||||
// repeat a name another plugin (or the same one) declared; its label and
|
||||
// description must not be empty, and Implies must be empty, read, or write.
|
||||
// Built-ins keep Implies empty. Any violation is an error, which fails boot.
|
||||
// Phrase keys are checked against the translator by Boot. Every name of a
|
||||
// catalog that compiles is added to the process-wide set the legacy
|
||||
// MintAPIToken accepts.
|
||||
func CompileAPITokenScopes(plugins []party.Plugin) (*APITokenScopeCatalog, error) {
|
||||
scopes := builtInAPITokenScopes()
|
||||
owner := map[string]string{}
|
||||
@@ -111,14 +136,22 @@ func CompileAPITokenScopes(plugins []party.Plugin) (*APITokenScopeCatalog, error
|
||||
}
|
||||
return nil, fmt.Errorf("golem15.user: duplicate member token scope %q declared by %s and %s", name, other, id)
|
||||
}
|
||||
if name == "backend" || name == "offline_access" {
|
||||
return nil, fmt.Errorf("golem15.user: plugin %s declared the reserved member token scope %q", id, name)
|
||||
}
|
||||
if strings.TrimSpace(scope.Label) == "" {
|
||||
return nil, fmt.Errorf("golem15.user: plugin %s member token scope %s has an empty label", id, name)
|
||||
}
|
||||
if strings.TrimSpace(scope.Description) == "" {
|
||||
return nil, fmt.Errorf("golem15.user: plugin %s member token scope %s has an empty description", id, name)
|
||||
}
|
||||
if scope.Implies != "" && scope.Implies != "read" && scope.Implies != "write" {
|
||||
return nil, fmt.Errorf("golem15.user: plugin %s member token scope %s has implies %q", id, name, scope.Implies)
|
||||
}
|
||||
owner[name] = id
|
||||
scopes = append(scopes, APITokenScope{Name: name, Label: scope.Label, Description: scope.Description, PluginID: id})
|
||||
scopes = append(scopes, APITokenScope{
|
||||
Name: name, Label: scope.Label, Description: scope.Description, PluginID: id, Implies: scope.Implies,
|
||||
})
|
||||
declared = append(declared, name)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user