Skip to content

Commit 63fc35e

Browse files
committed
Add permissions for render plugin
1 parent d22069d commit 63fc35e

File tree

15 files changed

+558
-20
lines changed

15 files changed

+558
-20
lines changed

contrib/render-plugins/example-wasm/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ then renders the result inside the file viewer.
1616
- `build.sh` — helper script that builds `plugin.wasm` and produces a zip
1717
archive ready for upload
1818

19+
As with other plugins, declare any Gitea endpoints or external hosts the WASM
20+
module needs to call inside the `permissions` array in `manifest.json`. Without
21+
an explicit entry, the plugin may only download the file that is currently being
22+
rendered.
23+
1924
## Build & Install
2025

2126
1. Build the WASM binary and zip archive:

contrib/render-plugins/example-wasm/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
"entry": "render.js",
88
"filePatterns": [
99
"*.wasmnote"
10-
]
10+
],
11+
"permissions": []
1112
}

contrib/render-plugins/example/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ as a quick way to validate the dynamic plugin system locally.
1010
- `render.js` — an ES module that exports a `render(container, fileUrl)`
1111
function; it downloads the source file and renders it in a styled `<pre>`
1212

13+
By default plugins may only fetch the file that is currently being rendered.
14+
If your plugin needs to contact Gitea APIs or any external services, list their
15+
domains under the `permissions` array in `manifest.json`. Requests to hosts that
16+
are not declared there will be blocked by the runtime.
17+
1318
## Build & Install
1419

1520
1. Create a zip archive that contains both files:

contrib/render-plugins/example/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"version": "1.0.0",
66
"description": "Simple sample plugin that renders .txt files with a custom color scheme.",
77
"entry": "render.js",
8-
"filePatterns": ["*.txt"]
8+
"filePatterns": ["*.txt"],
9+
"permissions": []
910
}

models/migrations/v1_26/v324.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func AddRenderPluginTable(x *xorm.Engine) error {
1818
Version string `xorm:"NOT NULL"`
1919
Description string `xorm:"TEXT"`
2020
Source string `xorm:"TEXT"`
21+
Permissions []string `xorm:"JSON"`
2122
Entry string `xorm:"NOT NULL"`
2223
FilePatterns []string `xorm:"JSON"`
2324
FormatVersion int `xorm:"NOT NULL DEFAULT 1"`

models/render/plugin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Plugin struct {
2020
Source string `xorm:"TEXT"`
2121
Entry string `xorm:"NOT NULL"`
2222
FilePatterns []string `xorm:"JSON"`
23+
Permissions []string `xorm:"JSON"`
2324
FormatVersion int `xorm:"NOT NULL DEFAULT 1"`
2425
Enabled bool `xorm:"NOT NULL DEFAULT false"`
2526
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`

modules/renderplugin/manifest.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Manifest struct {
2929
Description string `json:"description"`
3030
Entry string `json:"entry"`
3131
FilePatterns []string `json:"filePatterns"`
32+
Permissions []string `json:"permissions"`
3233
}
3334

3435
// Normalize validates mandatory fields and normalizes values.
@@ -71,9 +72,34 @@ func (m *Manifest) Normalize() error {
7172
}
7273
sort.Strings(cleanPatterns)
7374
m.FilePatterns = cleanPatterns
75+
76+
cleanPerms := make([]string, 0, len(m.Permissions))
77+
seenPerm := make(map[string]struct{}, len(m.Permissions))
78+
for _, perm := range m.Permissions {
79+
perm = strings.TrimSpace(strings.ToLower(perm))
80+
if perm == "" {
81+
continue
82+
}
83+
if !isValidPermissionHost(perm) {
84+
return fmt.Errorf("manifest permission %q is invalid; only plain domains optionally including a port are allowed", perm)
85+
}
86+
if _, ok := seenPerm[perm]; ok {
87+
continue
88+
}
89+
seenPerm[perm] = struct{}{}
90+
cleanPerms = append(cleanPerms, perm)
91+
}
92+
sort.Strings(cleanPerms)
93+
m.Permissions = cleanPerms
7494
return nil
7595
}
7696

97+
var permissionHostRegexp = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::[0-9]{1,5})?$`)
98+
99+
func isValidPermissionHost(value string) bool {
100+
return permissionHostRegexp.MatchString(value)
101+
}
102+
77103
// LoadManifest reads and validates the manifest.json file located under dir.
78104
func LoadManifest(dir string) (*Manifest, error) {
79105
manifestPath := filepath.Join(dir, "manifest.json")
@@ -103,4 +129,5 @@ type Metadata struct {
103129
AssetsBase string `json:"assetsBaseUrl"`
104130
FilePatterns []string `json:"filePatterns"`
105131
SchemaVersion int `json:"schemaVersion"`
132+
Permissions []string `json:"permissions"`
106133
}

modules/renderplugin/manifest_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func TestManifestNormalizeDefaults(t *testing.T) {
2727
assert.Equal(t, "example.plugin", manifest.ID)
2828
assert.Equal(t, "render.js", manifest.Entry)
2929
assert.Equal(t, []string{"*.TXT", "README.md"}, manifest.FilePatterns)
30+
assert.Empty(t, manifest.Permissions)
3031
}
3132

3233
func TestManifestNormalizeErrors(t *testing.T) {
@@ -50,6 +51,7 @@ func TestManifestNormalizeErrors(t *testing.T) {
5051
{"missing name", func(m *Manifest) { m.Name = "" }, "name is required"},
5152
{"missing version", func(m *Manifest) { m.Version = "" }, "version is required"},
5253
{"no patterns", func(m *Manifest) { m.FilePatterns = nil }, "at least one file pattern"},
54+
{"invalid permission", func(m *Manifest) { m.Permissions = []string{"http://bad"} }, "manifest permission"},
5355
}
5456

5557
for _, tt := range tests {
@@ -82,3 +84,18 @@ func TestLoadManifest(t *testing.T) {
8284
assert.Equal(t, "example", manifest.ID)
8385
assert.Equal(t, []string{"*.md", "*.txt"}, manifest.FilePatterns)
8486
}
87+
88+
func TestManifestNormalizePermissions(t *testing.T) {
89+
manifest := Manifest{
90+
SchemaVersion: SupportedManifestVersion,
91+
ID: "perm",
92+
Name: "perm",
93+
Version: "1.0.0",
94+
Entry: "render.js",
95+
FilePatterns: []string{"*.md"},
96+
Permissions: []string{" Example.com ", "api.example.com:8080", "example.com", ""},
97+
}
98+
99+
require.NoError(t, manifest.Normalize())
100+
assert.Equal(t, []string{"api.example.com:8080", "example.com"}, manifest.Permissions)
101+
}

options/locale/locale_en-US.ini

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3030,6 +3030,20 @@ render_plugins.detail.none = Not provided
30303030
render_plugins.detail.file_patterns_empty = No file patterns declared.
30313031
render_plugins.detail.actions = Plugin actions
30323032
render_plugins.detail.upgrade = Upgrade plugin
3033+
render_plugins.detail.permissions = Permissions
3034+
render_plugins.confirm_install = Review permissions before installing "%s"
3035+
render_plugins.confirm_upgrade = Review permissions before upgrading "%s"
3036+
render_plugins.confirm.description = Gitea will only allow this plugin to contact the domains listed below (plus the file being rendered). Continue only if you trust these endpoints.
3037+
render_plugins.confirm.permissions = Requested domains
3038+
render_plugins.confirm.permission_hint = If the list is empty the plugin will only fetch the file currently being rendered.
3039+
render_plugins.confirm.permission_none = None
3040+
render_plugins.confirm.archive = Archive
3041+
render_plugins.confirm.actions.install = Install Plugin
3042+
render_plugins.confirm.actions.upgrade = Upgrade Plugin
3043+
render_plugins.confirm.actions.cancel = Cancel Upload
3044+
render_plugins.upload_token_invalid = Plugin upload session expired. Please upload the archive again.
3045+
render_plugins.upload_discarded = Plugin upload discarded.
3046+
render_plugins.identifier_mismatch = Uploaded plugin identifier "%s" does not match "%s".
30333047
hooks = Webhooks
30343048
integrations = Integrations
30353049
authentication = Authentication Sources

0 commit comments

Comments
 (0)