feat(tokens): refuse minting mcp scopes without the MCP licence feature

This commit is contained in:
2026-09-09 08:36:26 +00:00
parent ac8e957859
commit d95f299562
+26
View File
@@ -4,10 +4,12 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/auth"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services"
"gitea.hostxtra.co.uk/vantage/vantage-shared/license"
"github.com/gin-gonic/gin"
)
@@ -109,6 +111,30 @@ func createToken(c *gin.Context) {
}
}
// The MCP scopes are refused without the licence feature, matching the
// guard-at-source thinking in services/packages.go rather than relying on
// RequireFeature at the /api/mcp group alone. That gate is a runtime one:
// without this, a licence downgrade leaves live agent credentials that
// authenticate, list as MCP tokens in the UI, and then fail mid-
// conversation with a 403 the model cannot explain. Refusing at minting
// means a token carrying mcp:* only ever existed while the feature did.
//
// Existing tokens are deliberately untouched by a downgrade: the route
// gate already stops them reaching the endpoint, and silently revoking
// credentials on a billing change is worse than refusing new ones.
if !services.GetLicenseState(auth.InstanceID(c)).Feature(license.FeatureMCP) {
for _, s := range body.Scopes {
if strings.HasPrefix(s, "mcp:") {
c.JSON(http.StatusForbidden, gin.H{
"error": "feature_unavailable",
"feature": license.FeatureMCP,
"code": "feature_unavailable",
})
return
}
}
}
if !services.SelectorNarrowerOrEqual(body.TagSelector, auth.ServerScope(c)) {
c.JSON(http.StatusForbidden, gin.H{
"error": "a token cannot reach servers its creator cannot reach",