Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/skills/listener-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Key behaviours:
| `sslverify` | `true` | SSL verification: `true`, `false`, or path to CA bundle |
| `limit` | — | Keep only the N most recent runs; older ones auto-deleted |
| `output` | — | Required when using pabot with a custom `-o` output filename |
| `user` | — | Username for basic authentication |
| `password` | — | Password for basic authentication |

---

Expand Down
20 changes: 10 additions & 10 deletions .github/skills/server-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ Served with **uvicorn** via `fastapi_offline.FastAPIOffline`.
## Authentication

- HTTP Basic Auth is implemented using `HTTPBasic` + `secrets.compare_digest` (constant-time comparison)
- **Only `/admin` requires credentials** when `server_user` and `server_pass` are set
- All other endpoints (`/add-outputs`, `/remove-outputs`, etc.) are **unauthenticated by design**
- If no credentials are configured, `/admin` is also open
- **`/admin` and all mutation endpoints require credentials** when `server_user` and `server_pass` are set
- Read-only endpoints (`/`, `/get-outputs`, `/get-logs`, `/log`, `/{full_path}`) are **unauthenticated**
- If no credentials are configured, all endpoints are open (the `authenticate` dependency returns `"anonymous"`)

---

Expand All @@ -36,15 +36,15 @@ Served with **uvicorn** via `fastapi_offline.FastAPIOffline`.
|---|---|---|---|
| `GET` | `/` | None | Serves `robot_dashboard.html` from the working directory |
| `GET` | `/admin` | Basic (if configured) | Serves the admin page HTML (generated from `templates/admin.html`) |
| `POST` | `/refresh-dashboard` | None | Manually triggers `robotdashboard.create_dashboard()` without adding data |
| `POST` | `/refresh-dashboard` | Basic (if configured) | Manually triggers `robotdashboard.create_dashboard()` without adding data |
| `GET` | `/get-outputs` | None | Returns list of `{run_start, name, alias, tags}` for all stored runs |
| `POST` | `/add-outputs` | None | Add output(s) from a path, raw XML string, or folder. See body fields below. |
| `POST` | `/add-output-file` | None | Multipart upload of `output.xml` (or `.gz`/`.gzip`). Form fields: `tags` (colon-separated), `version`. |
| `DELETE` | `/remove-outputs` | None | Remove runs by various selectors. See body fields below. |
| `POST` | `/add-outputs` | Basic (if configured) | Add output(s) from a path, raw XML string, or folder. See body fields below. |
| `POST` | `/add-output-file` | Basic (if configured) | Multipart upload of `output.xml` (or `.gz`/`.gzip`). Form fields: `tags` (colon-separated), `version`. |
| `DELETE` | `/remove-outputs` | Basic (if configured) | Remove runs by various selectors. See body fields below. |
| `GET` | `/get-logs` | None | Lists filenames in `robot_logs/` |
| `POST` | `/add-log` | None | Saves HTML log content to `robot_logs/<log_name>` and links it to the matching run in the DB |
| `POST` | `/add-log-file` | None | Same as `/add-log` but via multipart file upload (supports `.gz`/`.gzip`) |
| `DELETE` | `/remove-log` | None | Removes one log by `log_name`, or all logs with `all: True` |
| `POST` | `/add-log` | Basic (if configured) | Saves HTML log content to `robot_logs/<log_name>` and links it to the matching run in the DB |
| `POST` | `/add-log-file` | Basic (if configured) | Same as `/add-log` but via multipart file upload (supports `.gz`/`.gzip`) |
| `DELETE` | `/remove-log` | Basic (if configured) | Removes one log by `log_name`, or all logs with `all: True` |
| `GET` | `/log` | None | Serves a log HTML file by `?path=` query param; stores parent dir for subsequent resource requests |
| `GET` | `/{full_path:path}` | None | Catch-all: serves static resources (screenshots, etc.) relative to the last served log's directory. Path-traversal protected. |

Expand Down
19 changes: 10 additions & 9 deletions docs/dashboard-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,26 @@ The built-in server exposes several HTTP endpoints to manage and serve dashboard
| `/` | Serves the HTML dashboard (reflects current database), not callable through scripts |
| `/admin` | Admin page for manual management of runs and logs, not callable through scripts |
| `/get-outputs` | Returns a JSON list of stored runs (`run_start`, `alias`, `tags`), callable |
| `/add-outputs` | Accepts new output data via JSON (file path, raw XML or folder), callable |
| `/add-output-file` | Accepts new output data via file input, callable |
| `/remove-outputs` | Deletes runs by index, alias, `run_start`, tags, limit or 'all=true' for all outputs. Automatically deletes the associated log file from `robot_logs/` if one exists, callable |
| `/add-outputs` | Accepts new output data via JSON (file path, raw XML or folder), callable, **requires auth** |
| `/add-output-file` | Accepts new output data via file input, callable, **requires auth** |
| `/remove-outputs` | Deletes runs by index, alias, `run_start`, tags, limit or 'all=true' for all outputs. Automatically deletes the associated log file from `robot_logs/` if one exists, callable, **requires auth** |
| `/get-logs` | Returns a JSON list of stored logs on the server (`log_name`), callable |
| `/add-log` | Upload HTML a log file and associate them with runs (for [Log Linking](/log-linking.md)), callable |
| `/add-log-file` | Upload a HTML log file (for [Log Linking](/log-linking.md)), callable |
| `/remove-log` | Remove previously uploaded log files or provide 'all=true' for all logs, callable |
| `/refresh-dashboard` | Manually trigger regeneration of the dashboard HTML. Only needed when `--noautoupdate` is active, callable |
| `/add-log` | Upload HTML a log file and associate them with runs (for [Log Linking](/log-linking.md)), callable, **requires auth** |
| `/add-log-file` | Upload a HTML log file (for [Log Linking](/log-linking.md)), callable, **requires auth** |
| `/remove-log` | Remove previously uploaded log files or provide 'all=true' for all logs, callable, **requires auth** |
| `/refresh-dashboard` | Manually trigger regeneration of the dashboard HTML. Only needed when `--noautoupdate` is active, callable, **requires auth** |

All API endpoints are documented and described in the server’s own OpenAPI schema, accessible via the admin interface under “Swagger API Docs” or "Redoc API Docs", after starting the server.

## Security: Basic Auth (Optional)

If you start the server with a username and password, the admin page will be protected. Only someone providing the correct credentials can:
If you start the server with a username and password, the admin page **and all mutation endpoints** will be protected. Only someone providing the correct credentials can:

- Add or remove outputs manually
- Add or remove logs manually
- Trigger a dashboard refresh

The dashboard itself (the HTML) does **not** require authentication. API calls as of now do also **not** require authentication.
The dashboard itself (the HTML) does **not** require authentication. Read-only endpoints (`/get-outputs`, `/get-logs`) also do **not** require authentication.

## Security: HTTPS (Optional)

Expand Down
14 changes: 12 additions & 2 deletions docs/listener-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ robot --listener robotdashboardlistener.py:protocol=https:sslverify=false tests.
robot --listener robotdashboardlistener.py:protocol=https:sslverify=/path/to/ca-bundle.pem tests.robot
```

**With basic authentication**

```bash
robot --listener robotdashboardlistener.py:user=admin:password=secret tests.robot
```

## Full Listener Options

The listener supports the following arguments:
Expand All @@ -93,11 +99,13 @@ The listener supports the following arguments:
| `sslverify` | SSL certificate verification for HTTPS: `true` (default), `false` (skip verification for self-signed certs), or a path to a CA bundle file |
| `limit` | Maximum number of runs stored in the database (older runs will be auto-deleted, based on the order in the database) |
| `output` | Required only when using Pabot **with a custom output.xml name** |
| `user` | Username for basic authentication (optional) |
| `password` | Password for basic authentication (optional) |

**Example with all options**

```bash
robot --listener robotdashboardlistener.py:tags=dev1,dev2:version=v2.0:host=127.0.0.2:port=8888:protocol=https:sslverify=false:limit=100:uploadlog=true tests.robot
robot --listener robotdashboardlistener.py:tags=dev1,dev2:version=v2.0:host=127.0.0.2:port=8888:protocol=https:sslverify=false:limit=100:uploadlog=true:user=admin:password=secret tests.robot
```

## Using the Listener with Pabot
Expand Down Expand Up @@ -254,9 +262,11 @@ The arguments mirror those of `robotdashboardlistener.py`:
| `--protocol` | Protocol to use when connecting to the server: `http` or `https` (default: `http`) |
| `--sslverify` | SSL certificate verification for HTTPS: `true` (default), `false` (skip verification for self-signed certs), or a path to a CA bundle file |
| `--limit` | Maximum number of runs stored in the database (older runs will be auto-deleted) |
| `--user` | Username for basic authentication (optional) |
| `--password` | Password for basic authentication (optional) |

**Example with all options**

```bash
python robotdashboardscript.py --output path/to/output.xml --log path/to/log.html --tags dev1,dev2 --version v2.0 --host 127.0.0.2 --port 8888 --protocol https --sslverify false --limit 100
python robotdashboardscript.py --output path/to/output.xml --log path/to/log.html --tags dev1,dev2 --version v2.0 --host 127.0.0.2 --port 8888 --protocol https --sslverify false --limit 100 --user admin --password secret
```
8 changes: 8 additions & 0 deletions example/listener/robotdashboardlistener.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
# limit=100 - Max runs in database; auto-delete oldest (default: 0 = unlimited)
# output=custom.xml - Custom output filename (required for pabot with custom -o)
# customfilters=key=val:key=val - Custom filter key=value pairs (colon-separated)
# user=admin - Username for basic authentication (default: none)
# password=secret - Password for basic authentication (default: none)
#
# Features:
# - Output files are gzip-compressed and sent to /add-output-file
Expand All @@ -50,6 +52,8 @@ def __init__(
uploadlog: bool = False,
output: str = None, # this option is only required when using pabot and a custom output.xml name!
customfilters: str = None, # custom filter key=value pairs (colon-separated, e.g. key=val:key=val)
user: str = None,
password: str = None,
):
self.host = host
self.port = port
Expand All @@ -61,6 +65,7 @@ def __init__(
self.uploadlog = str(uploadlog).lower() == "true"
self.output = output if output != None else "output.xml"
self.customfilters = customfilters
self.auth = (user, password) if user and password else None
self.path: str
self.log_path: str
self.last_execution: str
Expand Down Expand Up @@ -147,6 +152,7 @@ def _add_output_to_database(self, path: str):
f"{self.protocol}://{self.host}:{self.port}/add-output-file",
data=form_data,
files=files,
auth=self.auth,
verify=self.ssl_verify,
)
except ConnectionError as e:
Expand Down Expand Up @@ -192,6 +198,7 @@ def _upload_log_file(self):
response = post(
f"{self.protocol}://{self.host}:{self.port}/add-log-file",
files=files,
auth=self.auth,
verify=self.ssl_verify,
)
except ConnectionError:
Expand All @@ -217,6 +224,7 @@ def _remove_runs_over_limit(self):
response = delete(
f"{self.protocol}://{self.host}:{self.port}/remove-outputs",
json=body,
auth=self.auth,
verify=self.ssl_verify,
)
if response.status_code == 200:
Expand Down
25 changes: 22 additions & 3 deletions example/script/robotdashboardscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
# --sslverify true SSL verification: 'true', 'false', or path to CA bundle (default: true)
# --limit 100 Keep only the N most recent runs; auto-delete the rest (default: 0 = unlimited)
# --customfilters key=val:key=val Custom filter key=value pairs, colon-separated (default: none)
# --user admin Username for basic authentication (default: none)
# --password secret Password for basic authentication (default: none)


def _parse_args():
Expand Down Expand Up @@ -87,6 +89,16 @@ def _parse_args():
default=None,
help="Custom filter key=value pairs, colon-separated (e.g. 'key=val:key=val').",
)
parser.add_argument(
"--user",
default=None,
help="Username for basic authentication (optional).",
)
parser.add_argument(
"--password",
default=None,
help="Password for basic authentication (optional).",
)
return parser.parse_args()


Expand All @@ -112,7 +124,7 @@ def _print_console_message(response):
_print_pusher(f"{message_line}")


def _add_output_to_server(output_path: str, tags, version, customfilters, host, port, protocol, ssl_verify):
def _add_output_to_server(output_path: str, tags, version, customfilters, host, port, protocol, ssl_verify, auth):
_print_pusher(f"starting processing output.xml '{output_path}'")
tags_list = tags.split(",") if tags else []
tags_str = ":".join(filter(None, tags_list)) if tags_list else ""
Expand All @@ -135,6 +147,7 @@ def _add_output_to_server(output_path: str, tags, version, customfilters, host,
f"{protocol}://{host}:{port}/add-output-file",
data=form_data,
files=files,
auth=auth,
verify=ssl_verify,
)
except ConnectionError:
Expand All @@ -156,7 +169,7 @@ def _add_output_to_server(output_path: str, tags, version, customfilters, host,
exit(1)


def _upload_log_file(log_path: str, host, port, protocol, ssl_verify):
def _upload_log_file(log_path: str, host, port, protocol, ssl_verify, auth):
if not exists(log_path):
_print_pusher(f"WARNING log file '{log_path}' not found, skipping log upload")
return
Expand All @@ -173,6 +186,7 @@ def _upload_log_file(log_path: str, host, port, protocol, ssl_verify):
response = post(
f"{protocol}://{host}:{port}/add-log-file",
files=files,
auth=auth,
verify=ssl_verify,
)
except ConnectionError:
Expand All @@ -193,12 +207,13 @@ def _upload_log_file(log_path: str, host, port, protocol, ssl_verify):
)


def _remove_runs_over_limit(limit: int, host, port, protocol, ssl_verify):
def _remove_runs_over_limit(limit: int, host, port, protocol, ssl_verify, auth):
body = {"limit": limit}
try:
response = delete(
f"{protocol}://{host}:{port}/remove-outputs",
json=body,
auth=auth,
verify=ssl_verify,
)
except ConnectionError:
Expand All @@ -218,6 +233,7 @@ def main():
args = _parse_args()
ssl_verify = _parse_ssl_verify(args.sslverify)
limit = int(args.limit)
auth = (args.user, args.password) if args.user and args.password else None

if not exists(args.output):
_print_pusher(f"ERROR output file '{args.output}' not found")
Expand All @@ -232,6 +248,7 @@ def main():
port=args.port,
protocol=args.protocol,
ssl_verify=ssl_verify,
auth=auth,
)

if args.log:
Expand All @@ -241,6 +258,7 @@ def main():
port=args.port,
protocol=args.protocol,
ssl_verify=ssl_verify,
auth=auth,
)

if limit > 0:
Expand All @@ -250,6 +268,7 @@ def main():
port=args.port,
protocol=args.protocol,
ssl_verify=ssl_verify,
auth=auth,
)


Expand Down
Loading
Loading