feat: update plugin permissions from allowedHosts to requiredHosts for better clarity and consistency

This commit is contained in:
Deluan 2026-01-12 17:48:04 -05:00
parent c0dd0f7282
commit 2e1166f718
18 changed files with 56 additions and 52 deletions

View File

@ -126,7 +126,7 @@ Every plugin must include a `manifest.json` file. Example:
"permissions": {
"http": {
"reason": "Fetch metadata from external API",
"allowedHosts": ["api.example.com", "*.musicbrainz.org"]
"requiredHosts": ["api.example.com", "*.musicbrainz.org"]
}
}
}
@ -300,7 +300,7 @@ Make HTTP requests using the Extism PDK's built-in HTTP support. See your [Extis
"permissions": {
"http": {
"reason": "Fetch metadata from external API",
"allowedHosts": ["api.example.com", "*.musicbrainz.org"]
"requiredHosts": ["api.example.com", "*.musicbrainz.org"]
}
}
}
@ -519,7 +519,7 @@ Establish persistent WebSocket connections to external services.
"permissions": {
"websocket": {
"reason": "Real-time connection to service",
"allowedHosts": ["gateway.example.com", "*.discord.gg"]
"requiredHosts": ["gateway.example.com", "*.discord.gg"]
}
}
}

View File

@ -7,7 +7,7 @@
"permissions": {
"http": {
"reason": "Fetch album cover art from Cover Art Archive API",
"allowedHosts": [
"requiredHosts": [
"coverartarchive.org",
"*.archive.org"
]

View File

@ -13,7 +13,7 @@
},
"websocket": {
"reason": "To connect to Coinbase WebSocket API for real-time prices",
"allowedHosts": ["ws-feed.exchange.coinbase.com"]
"requiredHosts": ["ws-feed.exchange.coinbase.com"]
}
}
}

View File

@ -10,11 +10,11 @@
},
"http": {
"reason": "To communicate with Discord API for gateway discovery and image uploads",
"allowedHosts": ["discord.com"]
"requiredHosts": ["discord.com"]
},
"websocket": {
"reason": "To maintain real-time connection with Discord gateway",
"allowedHosts": ["gateway.discord.gg"]
"requiredHosts": ["gateway.discord.gg"]
},
"cache": {
"reason": "To store connection state and sequence numbers"

View File

@ -10,13 +10,13 @@
},
"http": {
"reason": "To communicate with Discord API for gateway discovery and image uploads",
"allowedHosts": [
"requiredHosts": [
"discord.com"
]
},
"websocket": {
"reason": "To maintain real-time connection with Discord gateway",
"allowedHosts": [
"requiredHosts": [
"gateway.discord.gg"
]
},

View File

@ -7,7 +7,7 @@
"permissions": {
"http": {
"reason": "To send webhook notifications to configured URLs",
"allowedHosts": ["*"]
"requiredHosts": ["*"]
},
"users": {
"reason": "Receive scrobble events for users assigned to this plugin"

View File

@ -7,7 +7,7 @@
"permissions": {
"http": {
"reason": "Fetch metadata from Wikimedia APIs",
"allowedHosts": [
"requiredHosts": [
"query.wikidata.org",
"dbpedia.org",
"en.wikipedia.org"

View File

@ -54,9 +54,9 @@ type wsConnection struct {
// webSocketServiceImpl implements host.WebSocketService.
// It provides plugins with WebSocket communication capabilities.
type webSocketServiceImpl struct {
pluginName string
manager *Manager
allowedHosts []string
pluginName string
manager *Manager
requiredHosts []string
mu sync.RWMutex
connections map[string]*wsConnection
@ -65,10 +65,10 @@ type webSocketServiceImpl struct {
// newWebSocketService creates a new WebSocketService for a plugin.
func newWebSocketService(pluginName string, manager *Manager, permission *WebSocketPermission) *webSocketServiceImpl {
return &webSocketServiceImpl{
pluginName: pluginName,
manager: manager,
allowedHosts: permission.AllowedHosts,
connections: make(map[string]*wsConnection),
pluginName: pluginName,
manager: manager,
requiredHosts: permission.RequiredHosts,
connections: make(map[string]*wsConnection),
}
}
@ -248,7 +248,7 @@ func (s *webSocketServiceImpl) isHostAllowed(host string) bool {
hostWithoutPort = host[:idx]
}
for _, pattern := range s.allowedHosts {
for _, pattern := range s.requiredHosts {
if matchHostPattern(pattern, hostWithoutPort) {
return true
}

View File

@ -197,7 +197,7 @@ var _ = Describe("WebSocketService", Ordered, func() {
if idx := strings.LastIndex(serverURL, ":"); idx != -1 {
hostOnly = serverURL[:idx]
}
testService.allowedHosts = append(testService.allowedHosts, hostOnly)
testService.requiredHosts = append(testService.requiredHosts, hostOnly)
})
AfterEach(func() {
@ -324,7 +324,7 @@ var _ = Describe("WebSocketService", Ordered, func() {
if idx := strings.LastIndex(serverURL, ":"); idx != -1 {
hostOnly = serverURL[:idx]
}
testService.allowedHosts = append(testService.allowedHosts, hostOnly)
testService.requiredHosts = append(testService.requiredHosts, hostOnly)
})
AfterEach(func() {
@ -451,7 +451,7 @@ var _ = Describe("WebSocketService", Ordered, func() {
if idx := strings.LastIndex(serverURL, ":"); idx != -1 {
hostOnly = serverURL[:idx]
}
testService.allowedHosts = append(testService.allowedHosts, hostOnly)
testService.requiredHosts = append(testService.requiredHosts, hostOnly)
})
AfterEach(func() {
@ -542,7 +542,7 @@ var _ = Describe("WebSocketService", Ordered, func() {
if idx := strings.LastIndex(serverURL, ":"); idx != -1 {
hostOnly = serverURL[:idx]
}
testService.allowedHosts = append(testService.allowedHosts, hostOnly)
testService.requiredHosts = append(testService.requiredHosts, hostOnly)
ctx := GinkgoT().Context()
wsURL := "ws://" + serverURL

View File

@ -268,7 +268,7 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error {
Timeout: uint64(defaultTimeout.Milliseconds()),
}
if hosts := pkg.Manifest.AllowedHosts(); len(hosts) > 0 {
if hosts := pkg.Manifest.RequiredHTTPHosts(); len(hosts) > 0 {
pluginManifest.AllowedHosts = hosts
}

View File

@ -125,9 +125,9 @@
"type": "string",
"description": "Explanation for why HTTP access is needed"
},
"allowedHosts": {
"requiredHosts": {
"type": "array",
"description": "List of allowed host patterns for HTTP requests (e.g., 'api.example.com', '*.spotify.com')",
"description": "List of required host patterns for HTTP requests (e.g., 'api.example.com', '*.spotify.com')",
"items": {
"type": "string"
}
@ -176,9 +176,9 @@
"type": "string",
"description": "Explanation for why WebSocket access is needed"
},
"allowedHosts": {
"requiredHosts": {
"type": "array",
"description": "List of allowed host patterns for WebSocket connections (e.g., 'api.example.com', '*.spotify.com')",
"description": "List of required host patterns for WebSocket connections (e.g., 'api.example.com', '*.spotify.com')",
"items": {
"type": "string"
}

View File

@ -45,13 +45,13 @@ func ValidateWithCapabilities(m *Manifest, capabilities []Capability) error {
return nil
}
// AllowedHosts returns a list of allowed hosts for HTTP requests.
// RequiredHTTPHosts returns a list of required hosts for HTTP requests.
// Returns the hosts directly from the manifest's permissions.
func (m *Manifest) AllowedHosts() []string {
func (m *Manifest) RequiredHTTPHosts() []string {
if m.Permissions == nil || m.Permissions.Http == nil {
return nil
}
return m.Permissions.Http.AllowedHosts
return m.Permissions.Http.RequiredHosts
}
// HasExperimentalThreads returns true if the manifest requests experimental threads support.

View File

@ -31,12 +31,12 @@ type Experimental struct {
// HTTP access permissions for a plugin
type HTTPPermission struct {
// List of allowed host patterns for HTTP requests (e.g., 'api.example.com',
// '*.spotify.com')
AllowedHosts []string `json:"allowedHosts,omitempty" yaml:"allowedHosts,omitempty" mapstructure:"allowedHosts,omitempty"`
// Explanation for why HTTP access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
// List of required host patterns for HTTP requests (e.g., 'api.example.com',
// '*.spotify.com')
RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"`
}
// Key-value store permissions for persistent plugin storage
@ -189,10 +189,10 @@ type UsersPermission struct {
// WebSocket service permissions for establishing WebSocket connections
type WebSocketPermission struct {
// List of allowed host patterns for WebSocket connections (e.g.,
// 'api.example.com', '*.spotify.com')
AllowedHosts []string `json:"allowedHosts,omitempty" yaml:"allowedHosts,omitempty" mapstructure:"allowedHosts,omitempty"`
// Explanation for why WebSocket access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
// List of required host patterns for WebSocket connections (e.g.,
// 'api.example.com', '*.spotify.com')
RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"`
}

View File

@ -19,7 +19,7 @@ var _ = Describe("Manifest", func() {
"permissions": {
"http": {
"reason": "Fetch metadata",
"allowedHosts": ["api.example.com", "*.spotify.com"]
"requiredHosts": ["api.example.com", "*.spotify.com"]
}
}
}`)
@ -34,7 +34,7 @@ var _ = Describe("Manifest", func() {
Expect(*m.Website).To(Equal("https://example.com"))
Expect(m.Permissions.Http).ToNot(BeNil())
Expect(*m.Permissions.Http.Reason).To(Equal("Fetch metadata"))
Expect(m.Permissions.Http.AllowedHosts).To(ContainElements("api.example.com", "*.spotify.com"))
Expect(m.Permissions.Http.RequiredHosts).To(ContainElements("api.example.com", "*.spotify.com"))
})
It("parses a minimal manifest", func() {
@ -117,11 +117,11 @@ var _ = Describe("Manifest", func() {
})
})
Describe("AllowedHosts", func() {
Describe("RequiredHTTPHosts", func() {
It("returns nil when no permissions", func() {
m := &Manifest{}
Expect(m.AllowedHosts()).To(BeNil())
Expect(m.RequiredHTTPHosts()).To(BeNil())
})
It("returns nil when no HTTP permissions", func() {
@ -129,19 +129,19 @@ var _ = Describe("Manifest", func() {
Permissions: &Permissions{},
}
Expect(m.AllowedHosts()).To(BeNil())
Expect(m.RequiredHTTPHosts()).To(BeNil())
})
It("returns hosts from permissions", func() {
m := &Manifest{
Permissions: &Permissions{
Http: &HTTPPermission{
AllowedHosts: []string{"api.example.com", "*.spotify.com"},
RequiredHosts: []string{"api.example.com", "*.spotify.com"},
},
},
}
hosts := m.AllowedHosts()
hosts := m.RequiredHTTPHosts()
Expect(hosts).To(Equal([]string{"api.example.com", "*.spotify.com"}))
})
})

View File

@ -6,7 +6,7 @@
"permissions": {
"websocket": {
"reason": "For testing WebSocket callbacks",
"allowedHosts": ["*.example.com", "localhost:*", "echo.websocket.org"]
"requiredHosts": ["*.example.com", "localhost:*", "echo.websocket.org"]
}
}
}

View File

@ -395,7 +395,8 @@
"usersRequired": "Este plugin requer acesso a informações de usuário. Selecione quais usuários o plugin pode acessar, ou habilite 'Permitir todos os usuários'.",
"allLibrariesHelp": "Quando habilitado, o plugin terá acesso a todas as bibliotecas, incluindo as criadas no futuro.",
"noLibraries": "Nenhuma biblioteca selecionada",
"librariesRequired": "Este plugin requer acesso a informações de bibliotecas. Selecione quais bibliotecas o plugin pode acessar, ou habilite 'Permitir todas as bibliotecas'."
"librariesRequired": "Este plugin requer acesso a informações de bibliotecas. Selecione quais bibliotecas o plugin pode acessar, ou habilite 'Permitir todas as bibliotecas'.",
"requiredHosts": "Hosts necessários"
},
"placeholders": {
"configKey": "chave",

View File

@ -395,7 +395,8 @@
"usersRequired": "This plugin requires access to user information. Select which users the plugin can access, or enable 'Allow all users'.",
"allLibrariesHelp": "When enabled, the plugin will have access to all libraries, including those created in the future.",
"noLibraries": "No libraries selected",
"librariesRequired": "This plugin requires access to library information. Select which libraries the plugin can access, or enable 'Allow all libraries'."
"librariesRequired": "This plugin requires access to library information. Select which libraries the plugin can access, or enable 'Allow all libraries'.",
"requiredHosts": "Required hosts"
},
"placeholders": {
"configKey": "key",

View File

@ -10,15 +10,17 @@ import {
Link,
ClickAwayListener,
} from '@material-ui/core'
import { useTranslate } from 'react-admin'
import { DateField } from '../common'
// Helper component for permission chips with clickable persistent tooltips
const PermissionChip = ({ label, permission, classes }) => {
const [open, setOpen] = useState(false)
const translate = useTranslate()
if (!permission) return null
const hasHosts = permission.allowedHosts?.length > 0
const hasHosts = permission.requiredHosts?.length > 0
const hasTooltip = permission.reason || hasHosts
const handleClick = () => {
@ -39,8 +41,8 @@ const PermissionChip = ({ label, permission, classes }) => {
{hasHosts && (
<Box mt={permission.reason ? 0.5 : 0}>
<Typography variant="caption" component="div">
Allowed hosts:{' '}
{permission.allowedHosts.map((host, i) => (
{translate('resources.plugin.messages.requiredHosts')}:{' '}
{permission.requiredHosts.map((host, i) => (
<span key={host}>
{i > 0 && ', '}
<code>{host}</code>