Skip to content

Commit f67cc8b

Browse files
committed
Added missed file
1 parent 6c7c14b commit f67cc8b

15 files changed

Lines changed: 663 additions & 45 deletions

File tree

docs/content/docs/Actions.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,35 @@ Actions allow apps to expose an autogenerated UI for simple backend actions. For
1111
First, define the parameters to be exposed in the form UI. Create a `params.star` file with the [params]({{< ref "/docs/develop/#app-parameters" >}}). For example,
1212

1313
```python {filename="params.star"}
14-
param("dir", description="The directory to list files from", default="/tmp")
14+
param("repo", description="The GitHub repository to look up", default="openrundev/openrun")
1515

16-
param("detail", type=BOOLEAN, description="Whether to show file details", default=True)
16+
param("show_issues", type=BOOLEAN, description="Whether to show the open issues count", default=True)
1717
```
1818

19-
This app defines a run handler which runs `ls` on the specified directory. The output text is returned.
19+
This app defines a run handler which calls the GitHub API for the specified repository, using the [http plugin]({{< ref "docs/plugins/overview" >}}), and returns the stats as text.
2020

2121
```python {filename="app.star"}
22-
load ("exec.in", "exec")
22+
load ("http.in", "http")
2323

2424
def run(dry_run, args):
25-
if args.dir == "." or args.dir.startswith("./") or args.dir == ".." or args.dir.startswith("../"):
26-
return ace.result("Validation failed", param_errors={"dir": "relative paths not supported"})
25+
if "/" not in args.repo:
26+
return ace.result("Validation failed", param_errors={"repo": "expected owner/name format"})
2727

28-
cmd_args = ["-Lla" if args.detail else "-La", args.dir]
29-
out = exec.run("ls", cmd_args).value
30-
return ace.result("File listing for " + args.dir, out)
28+
repo = http.get("https://api.github.com/repos/" + args.repo).value.json()
29+
out = ["Stars: %d" % repo["stargazers_count"], "Forks: %d" % repo["forks_count"]]
30+
if args.show_issues:
31+
out.append("Open Issues: %d" % repo["open_issues_count"])
32+
return ace.result("Repo info for " + args.repo, out)
3133

32-
app = ace.app("List Files",
33-
actions=[ace.action("List Files", "/", run, description="Show the ls -a output for specified directory")],
34+
app = ace.app("Repo Info",
35+
actions=[ace.action("Repo Info", "/", run, description="Show the GitHub stats for the specified repository")],
3436
permissions=[
35-
ace.permission("exec.in", "run", ["ls"]),
37+
ace.permission("http.in", "get", ["regex:^https://api\\.github\\.com/.*"]),
3638
],
3739
)
3840
```
3941

40-
The app, when accessed will look as shown below, with the `ls` command output displayed:
42+
When accessed, the app shows a form for the params, with the action output displayed below it. For example, a file listing action app looks like:
4143

4244
<picture class="responsive-picture" style="display: block; margin-left: auto; margin-right: auto;">
4345
<source media="(prefers-color-scheme: dark)" srcset="/images/list_filesd_dark.png">

docs/content/docs/App/Overview.md

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,20 @@ This section of the docs covers Starlark and Hybrid apps. For a containerized ap
2121

2222
### Sample Starlark App
2323

24-
To create an app with a custom HTML page which shows a listing of files in your root directory, create an `~/myapp4/app.star` file with
24+
To create an app with a custom HTML page which lists the repositories of a GitHub org, create an `~/myapp4/app.star` file with
2525

2626
```python {filename="app.star"}
27-
load("exec.in", "exec")
27+
load("http.in", "http")
2828

2929
def handler(req):
30-
ret = exec.run("ls", ["-l", "/"]).value
31-
return {"Error": "", "Lines": ret}
30+
repos = http.get("https://api.github.com/orgs/openrundev/repos").value.json()
31+
lines = ["%s : %d stars" % (repo["name"], repo["stargazers_count"]) for repo in repos]
32+
return {"Error": "", "Lines": lines}
3233

3334
app = ace.app("hello4",
3435
custom_layout=True,
3536
routes = [ace.html("/")],
36-
permissions = [ace.permission("exec.in", "run", ["ls"])]
37+
permissions = [ace.permission("http.in", "get", ["regex:^https://api\\.github\\.com/.*"])]
3738
)
3839
```
3940

@@ -44,7 +45,7 @@ and an `~/myapp4/index.go.html` file with
4445
<!doctype html>
4546
<html>
4647
<head>
47-
<title>File List</title>
48+
<title>Repo List</title>
4849
{{ template "openrun_gen_import" . }}
4950
</head>
5051
<body>
@@ -61,11 +62,7 @@ and an `~/myapp4/index.go.html` file with
6162

6263
Run `openrun app create --auth=none --dev --approve ~/myapp4 /hello4`. After that, the app is available at `/hello4`. Note that the `--dev` option is required for the `openrun_gen_import` file to be generated which is required for live reload.
6364

64-
This app uses the `exec` plugin to run the ls command. The output of the command is shown when the app is accessed. To allow the app to run the plugin command, use the `openrun app approve` command.
65-
66-
{{<callout type="warning" >}}
67-
**Note:** If running on Windows, change `ls` to `dir`. Else, use the `fs` plugin to make this platform independent. See https://github.com/openrundev/apps/blob/main/system/disk_usage/app.star.
68-
{{</callout>}}
65+
This app uses the `http` plugin to call the GitHub API. The repo listing is shown when the app is accessed. To allow the app to make the plugin call, use the `openrun app approve` command.
6966

7067
### Custom Layout HTML App
7168

docs/content/docs/Applications/AppSecurity.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ This security model allows for the following:
3131

3232
As an example, the disk usage analysis app requires [two permissions](https://github.com/openrundev/openrun/blob/8b8975cea2d650c9f80dab6eb70cc5b2ddbe5c40/examples/disk_usage/app.star#L42)
3333

34+
{{<callout type="warning" >}}
35+
**Note:** The `exec.in` plugin is disallowed by default at the server level (`permissions.disallow`) since it runs commands on the server host, and it requires an authenticated caller. To run apps that use it, like this example, see [default plugin permissions]({{< ref "/docs/configuration/security/#default-plugin-permissions" >}}).
36+
{{</callout>}}
37+
3438
```python {filename="app.star"}
3539
app = ace.app("Disk Usage",
3640
routes=[ace.html("/", partial="du_table_block")],

docs/content/docs/Configuration/Security.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ secrets = []
9696

9797
The `http.in` default only auto-allows outbound HTTP calls to the app's own container (`<CONTAINER_URL>...`), so a Starlark frontend can call its own backend without an approval. The pattern is anchored with `^` because the argument matcher is unanchored — without it an app could embed `<CONTAINER_URL>` inside an external URL to bypass the restriction. Calls to any other host fall back to requiring an approved `http.in` permission. To allow all outbound HTTP (which opens an SSRF surface, since these calls originate from the server's network position and can reach internal or metadata endpoints), use `arguments = ["regex:.*"]`; to allow specific hosts, list them, e.g. `arguments = ["regex:^https://api\\.example\\.com/.*"]`.
9898

99+
`permissions.disallow` blocks matching plugin calls for **every app**, even when the app's approved permissions (or the `permissions.allow` list) would permit them. Entries match with the same options as `allow`: an empty `method` matches every method of the plugin, and `arguments` (exact or `regex:`) narrow the block to matching calls. By default the `exec.in` plugin is disallowed, since it runs arbitrary commands on the server host:
100+
101+
```toml {filename="openrun.toml"}
102+
[[permissions.disallow]]
103+
plugin = "exec.in"
104+
```
105+
106+
To enable exec for apps, override the list in `openrun.toml` — an empty list clears the default (`disallow = []` under `[permissions]`). The exec plugin is additionally a privileged system plugin: like `openrun_admin` and `build`, it requires an authenticated (non-anonymous) caller unless `security.unsafe_allow_system_plugins_anon` is set.
107+
99108
The default OpenRun server config already includes two implicit approvals used by containerized apps:
100109

101110
- `proxy.config(container.URL, ...)`

docs/content/docs/Develop.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,27 +120,29 @@ For containerized apps, all params specified for the app (including ones not spe
120120
For use cases where an existing CLI application or API needs to be exposed as a web app, actions provide an easy solution. First, define the parameters to be exposed in the form UI. Create a `params.star` file with the params. For example,
121121

122122
```python {filename="params.star"}
123-
param("dir", description="The directory to list files from", default="/tmp")
123+
param("repo", description="The GitHub repository to look up", default="openrundev/openrun")
124124
```
125125

126-
The app defines a run handler which runs `ls` on the specified directory. The output text is returned.
126+
The app defines a run handler which calls the GitHub API for the specified repository, using the [http plugin]({{< ref "docs/plugins/overview" >}}), and returns the stats as text.
127127

128128
```python {filename="app.star"}
129-
load ("exec.in", "exec")
129+
load ("http.in", "http")
130130

131131
def run(dry_run, args):
132-
out = exec.run("ls", ["-Lla"]).value
133-
return ace.result("File listing for " + args.dir, out)
132+
repo = http.get("https://api.github.com/repos/" + args.repo).value.json()
133+
out = ["Stars: %d" % repo["stargazers_count"], "Forks: %d" % repo["forks_count"],
134+
"Open Issues: %d" % repo["open_issues_count"]]
135+
return ace.result("Repo info for " + args.repo, out)
134136

135-
app = ace.app("List Files",
136-
actions=[ace.action("List Files", "/", run, description="Show the ls -a output for specified directory")],
137+
app = ace.app("Repo Info",
138+
actions=[ace.action("Repo Info", "/", run, description="Show the GitHub stats for the specified repository")],
137139
permissions=[
138-
ace.permission("exec.in", "run", ["ls"]),
140+
ace.permission("http.in", "get", ["regex:^https://api\\.github\\.com/.*"]),
139141
],
140142
)
141143
```
142144

143-
The app, when accessed, will look as shown below, with the `ls` command output displayed:
145+
When accessed, the app shows a form for the params, with the action output displayed below it. For example, a file listing action app looks like:
144146

145147
<picture class="responsive-picture" style="display: block; margin-left: auto; margin-right: auto;">
146148
<source media="(prefers-color-scheme: dark)" srcset="/images/list_files_dark.png">

docs/content/docs/QuickStart.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,27 +120,29 @@ See [containerized apps]({{< ref "container/overview/" >}}) for details.
120120
For use cases where an existing CLI application or API needs to be exposed as a web app, actions provide an easy solution. First, define the parameters to be exposed in the form UI. Create a `params.star` file with the params. For example,
121121

122122
```python {filename="params.star"}
123-
param("dir", description="The directory to list files from", default="/tmp")
123+
param("repo", description="The GitHub repository to look up", default="openrundev/openrun")
124124
```
125125

126-
The app defines a run handler which runs `ls` on the specified directory. The output text is returned.
126+
The app defines a run handler which calls the GitHub API for the specified repository, using the [http plugin]({{< ref "docs/plugins/overview" >}}), and returns the stats as text.
127127

128128
```python {filename="app.star"}
129-
load ("exec.in", "exec")
129+
load ("http.in", "http")
130130

131131
def run(dry_run, args):
132-
out = exec.run("ls", ["-Lla"]).value
133-
return ace.result("File listing for " + args.dir, out)
132+
repo = http.get("https://api.github.com/repos/" + args.repo).value.json()
133+
out = ["Stars: %d" % repo["stargazers_count"], "Forks: %d" % repo["forks_count"],
134+
"Open Issues: %d" % repo["open_issues_count"]]
135+
return ace.result("Repo info for " + args.repo, out)
134136

135-
app = ace.app("List Files",
136-
actions=[ace.action("List Files", "/", run, description="Show the ls -a output for specified directory")],
137+
app = ace.app("Repo Info",
138+
actions=[ace.action("Repo Info", "/", run, description="Show the GitHub stats for the specified repository")],
137139
permissions=[
138-
ace.permission("exec.in", "run", ["ls"]),
140+
ace.permission("http.in", "get", ["regex:^https://api\\.github\\.com/.*"]),
139141
],
140142
)
141143
```
142144

143-
The app, when accessed will look as shown below, with the `ls` command output displayed:
145+
When accessed, the app shows a form for the params, with the action output displayed below it. For example, a file listing action app looks like:
144146

145147
<picture class="responsive-picture" style="display: block; margin-left: auto; margin-right: auto;">
146148
<source media="(prefers-color-scheme: dark)" srcset="/images/list_files_dark.png">

internal/app/plugin.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,17 @@ func (a *App) pluginHook(appPath, modulePath, accountName, functionName string,
318318
return nil, fmt.Errorf("plugin %s requires an authenticated user", modulePath)
319319
}
320320

321+
// Server config disallow list: a matching entry blocks the call for
322+
// every app, even when the app's approved permissions or the server
323+
// config allow list would permit it
324+
disallowed, err := matchesDisallowed(a.serverConfig.Permissions.Disallow, modulePath, functionName, args)
325+
if err != nil {
326+
return nil, err
327+
}
328+
if disallowed {
329+
return nil, fmt.Errorf("app %s is not permitted to call %s.%s: the call is disallowed by the server config (permissions.disallow)", a.Path, modulePath, functionName)
330+
}
331+
321332
permsList := append([]types.Permission(nil), a.Metadata.Permissions...)
322333
if len(a.serverConfig.Permissions.Allow) > 0 {
323334
// Add the server config allowed permissions to the list
@@ -473,6 +484,46 @@ func (a *App) pluginHook(appPath, modulePath, accountName, functionName string,
473484
return starlark.NewBuiltin(functionName, hook)
474485
}
475486

487+
// matchesDisallowed reports whether the plugin call matches a server config
488+
// permissions.disallow entry. Matching mirrors the allow/approval options in
489+
// checkPermissions: the plugin name must match, an empty method matches every
490+
// method of the plugin, and argument patterns (exact or regex:, positional
491+
// prefix) narrow the block to matching calls. A call with fewer arguments than
492+
// the entry's patterns does not match. Permit/is_read/secrets have no meaning
493+
// on a deny rule and are ignored
494+
func matchesDisallowed(disallowList []types.Permission, modulePath, functionName string, args starlark.Tuple) (bool, error) {
495+
for _, p := range disallowList {
496+
if p.Plugin != modulePath {
497+
continue
498+
}
499+
if p.Method != "" && p.Method != functionName {
500+
continue
501+
}
502+
if len(p.Arguments) > len(args) {
503+
continue
504+
}
505+
argMismatch := false
506+
for i, arg := range p.Arguments {
507+
funcInput := types.StripQuotes(args[i].String())
508+
if funcInput == arg {
509+
continue
510+
}
511+
match, err := types.RegexMatch(arg, funcInput)
512+
if err != nil {
513+
return false, err
514+
}
515+
if !match {
516+
argMismatch = true
517+
break
518+
}
519+
}
520+
if !argMismatch {
521+
return true, nil
522+
}
523+
}
524+
return false, nil
525+
}
526+
476527
func checkPermissions(ctx context.Context, a *App, modulePath string, functionName string, args starlark.Tuple, pluginInfo *plugin.PluginInfo, permsList []types.Permission) (error, [][]string, bool, error) {
477528
var lastError error
478529
secrets := [][]string{}

internal/app/tests/response_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package app_test
55

66
import (
7+
"context"
78
"encoding/json"
89
"net/http/httptest"
910
"testing"
@@ -249,6 +250,8 @@ def handler(req):
249250
t.Fatalf("Error %s", err)
250251
}
251252
request := httptest.NewRequest("GET", "/test", nil)
253+
// exec is a system plugin: an authenticated caller is required
254+
request = request.WithContext(context.WithValue(request.Context(), types.USER_ID, "testuser"))
252255
response := httptest.NewRecorder()
253256
a.ServeHTTP(response, request)
254257

@@ -273,6 +276,8 @@ def handler(req):
273276
t.Fatalf("Error %s", err)
274277
}
275278
request := httptest.NewRequest("GET", "/test", nil)
279+
// exec is a system plugin: an authenticated caller is required
280+
request = request.WithContext(context.WithValue(request.Context(), types.USER_ID, "testuser"))
276281
response := httptest.NewRecorder()
277282
a.ServeHTTP(response, request)
278283

0 commit comments

Comments
 (0)