mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat: update plugin permissions from allowedHosts to requiredHosts for better clarity and consistency
This commit is contained in:
parent
c0dd0f7282
commit
2e1166f718
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"permissions": {
|
||||
"http": {
|
||||
"reason": "Fetch album cover art from Cover Art Archive API",
|
||||
"allowedHosts": [
|
||||
"requiredHosts": [
|
||||
"coverartarchive.org",
|
||||
"*.archive.org"
|
||||
]
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
]
|
||||
},
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"permissions": {
|
||||
"http": {
|
||||
"reason": "Fetch metadata from Wikimedia APIs",
|
||||
"allowedHosts": [
|
||||
"requiredHosts": [
|
||||
"query.wikidata.org",
|
||||
"dbpedia.org",
|
||||
"en.wikipedia.org"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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"}))
|
||||
})
|
||||
})
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user