API
Every function this app publishes over HTTP, and what each one checks before it
does anything.
Who this is for. Somebody calling BenchPress from another program, or adding
an endpoint and wondering what the house rule is.
Before you start. Read How an endpoint is guarded.
The decorator publishes a function. It grants nothing. Every entry in the tables
below names its own check, because there is no check anywhere else.
Calling convention
Every endpoint is a Frappe whitelisted method. Call it by dotted path.
curl -s https://<host>/api/method/benchpress.api.get_labs \
-H "Cookie: sid=<session id>"
Arguments go as query parameters on a GET or as form fields on a POST.
Frappe decides the method from the function, not from a route table.
Thirty-five of the fifty are in benchpress.api. Four more are module functions
elsewhere. The remaining eleven are document methods, called through
run_doc_method rather than by path. All three kinds are marked below.
How an endpoint is guarded
Five helpers in benchpress/permissions.py do the work. An endpoint calls one
of them as its first statement.
| Helper | Passes for | Raises |
|---|---|---|
require_app_user() |
System Manager, BenchPress Admin, BenchPress User |
PermissionError |
require_admin() |
System Manager, BenchPress Admin |
PermissionError |
require_bench_access(name) |
whoever may read that Bench Instance |
PermissionError |
is_admin() |
returns a boolean, raises nothing | — |
has_app_permission() |
returns a boolean, raises nothing | — |
require_bench_access calls frappe.has_permission on the document itself. It
scopes because the BenchPress User permission row on Bench Instance carries
if_owner: 1. See How a row is scoped.
A second gate sits above some endpoints. requires_admission is a decorator in
benchpress/credits/guard.py. It refuses an action before any work is queued,
and it does nothing at all while enable_credits is 0.
| Decorator argument | Refuses when |
|---|---|
cost= |
the caller cannot pay the hold |
caps= |
the caller is at a cap — concurrency, size, devices or builds a day |
payer= |
the charge belongs to somebody other than the caller |
Two endpoints answer a guest. They are listed separately in
The two guest endpoints, and both are rate limited.
Labs and templates
| Endpoint | Arguments | Returns | Checks |
|---|---|---|---|
get_labs |
— | every lab the caller may see | require_app_user |
get_lab |
name |
one lab, with its instance and recent runs | require_app_user |
get_lab_form_options |
— | the enums and defaults the New lab form builds itself from | require_admin |
get_lab_templates |
— | the catalog | require_app_user |
create_lab_from_template |
template, lab_id, title |
{name, status: "Draft"} |
require_admin |
build_lab_image |
lab_name |
{name, status: "Building"} |
require_admin, plus requires_admission for cost and the builds-a-day cap |
build_lab_golden |
lab_name |
{name, status: "Queued"} |
require_admin |
prewarm_catalog |
— | how many builds were queued | require_admin |
build_lab_image and build_lab_golden queue work and return at once. Watch
the Build Log for the result.
get_lab_form_options is admin-only because only an admin reaches the New lab
screen. Do not read it as a public enum list.
Benches
| Endpoint | Arguments | Returns | Checks |
|---|---|---|---|
get_benches |
— | the caller's benches, or every bench for an admin | require_app_user |
create_bench |
data (JSON) |
{name, status: "Deploying"} |
require_app_user, plus requires_admission for the lease cost and the size cap |
bench_action |
bench_name, action |
{name, status} |
require_bench_access, and is_admin for delete |
server_time |
— | {server_now_ms} |
require_app_user |
bench_action takes four actions: start, restart, stop and delete.
| Action | Extra rule |
|---|---|
start, restart |
goes through requires_admission, so a capped caller is refused |
stop |
no gate. Stopping is what a refused caller is being told to do |
delete |
admin only, and it raises PermissionError for anybody else |
start, restart and stop all refuse an instance with no container. A Draft
bench has never been deployed, and a reaped one has had its container removed.
create_bench is idempotent for a pair of caller and lab. The bench name is an
MD5 of the two, so a second call redeploys rather than making a second bench.
server_time exists so a countdown in the browser corrects against the server
clock instead of the laptop's.
History
| Endpoint | Arguments | Returns | Checks |
|---|---|---|---|
get_deploy_logs |
bench_name |
the log rows for one bench | require_bench_access |
get_deploy_history |
— | up to 50 recent deploy runs | require_app_user, inside run_history |
get_build_history |
— | up to 50 recent image builds | require_app_user, inside run_history |
The two history endpoints do not check permissions in api.py. They delegate
immediately to benchpress/run_history.py, and the check is the first statement
there. This is the one place where the guard is a function call away, so the
table names where it lives.
Both are endpoints rather than a generic list read for a reason.
Build Log had no query condition, and reading it through the generic list API
served every user's image builds to everybody. Scoping cannot be added to a
generic read, so it is applied here: deploy rows go through frappe.get_list
and pick up deploy_log_query_conditions, and build rows are filtered by owner
for anybody who is not an admin.
Both answers carry window_days, limit and truncated. Logs are cleared
after seven days, so neither table is complete, and the answer says so rather
than implying otherwise.
Devices
Every device endpoint is a wrapper over vpn_management. A device is a
VPN Peer, not a BenchPress DocType.
| Endpoint | Arguments | Returns | Checks |
|---|---|---|---|
list_devices |
— | the caller's peers, with status and traffic counters | require_app_user |
add_device |
device_name, device_type, public_key |
the new peer, and its config when the server generated the key | require_app_user, plus requires_admission for the device cap |
remove_device |
device_name |
{status: "removed"} |
require_app_user |
get_device_types |
— | the seven accepted types | require_app_user |
get_device_wg_config |
device_name |
the WireGuard configuration text | require_app_user |
get_vpn_status |
— | whether the caller's tunnel is up | require_app_user |
run_connection_test |
— | a verdict on the caller's own peer | require_app_user |
get_device_types exists so no screen hand-types the list.
run_connection_test tests the caller's peer and never the shared
infrastructure.
Credentials
Three endpoints return secrets. All three take a bench name and check access to
that bench.
| Endpoint | Arguments | Returns | Checks |
|---|---|---|---|
get_bench_credentials |
bench_name |
the SSH user, the SSH password and the site admin password | require_bench_access |
get_code_server_credentials |
bench_name |
{url, password} |
require_bench_access |
restart_code_server |
bench_name |
{ok: true} |
require_bench_access |
These exist because Frappe does not return Password fields in an ordinary
document read. Reading the Bench Instance gives you nothing. Ask here.
Credits
Every endpoint below works while enable_credits is 0. They report that
credits are off rather than failing.
| Endpoint | Arguments | Returns | Checks |
|---|---|---|---|
get_user_context |
— | who the caller is, their roles, and whether credits exist | reads roles with is_admin, no raise |
get_credit_summary |
— | the balance chip, in one indexed read | require_app_user |
get_credit_statement |
limit_start, limit_page_length |
one page of the caller's own ledger | require_app_user |
get_purchase_options |
— | the packs for sale, and whether a gateway exists | require_app_user |
buy_credits |
pack |
a Razorpay order | require_app_user |
get_lease_plans |
— | the durations the renew dialog offers | require_app_user |
renew_bench |
bench_name, plan, request_id |
the new lease state | require_bench_access and require_balance |
get_user_context is the one endpoint with no raising guard, and that is
deliberate. It answers what the session already knows about itself. It calls
frappe.get_roles and is_admin, and it returns no other user's data.
get_credit_statement filters on the session, never on an argument. There is no
way to ask for another user's ledger.
renew_bench takes a request_id. A retried call carrying the same id does not
charge twice. The new deadline extends from the deadline the caller already had,
not from now.
buy_credits prices from the pack, never from the caller. See
Credits and billing for the gateway.
Screens
| Endpoint | Arguments | Returns | Checks |
|---|---|---|---|
get_overview |
— | every tile on the Overview screen, in one request | require_app_user |
Diagnostics
| Endpoint | Arguments | Returns | Checks |
|---|---|---|---|
run_diagnostics |
— | eleven read-only environment checks | require_admin |
preflight_runtime |
runtime |
whether that runtime can actually start a container | require_admin |
run_diagnostics reads. preflight_runtime creates a container, which is why
it is separate and why it is admin-only. Neither reads the queue workers. See
Diagnostics.
Document methods
Eleven whitelisted methods live on controllers rather than in api.py. Frappe
calls them through run_doc_method, and it checks the document's own permission
before the method runs.
Bench Instance
Four methods, used from the Desk form. None calls a permission helper. The
document check plus if_owner on the BenchPress User role is the guard.
| Method | Does | Extra gate |
|---|---|---|
enqueue_deploy |
queues a deploy on queue-long |
requires_admission, lease cost and size cap |
enqueue_redeploy |
queues a redeploy | requires_admission, lease cost and size cap |
enqueue_start |
starts an existing container | requires_admission, instance lease cost |
enqueue_stop |
stops through lifecycle.stopped |
none, for the same reason bench_action stop has none |
enqueue_deploy and enqueue_redeploy deduplicate on a job id of
deploy_bench:<name>. A second click while a deploy runs says so and queues
nothing.
Database Server
Five methods. Each one calls self.has_permission and throws, so the check is
explicit here rather than inherited.
| Method | Does |
|---|---|
setup_mariadb |
creates the shared MariaDB container |
start_mariadb |
starts it |
stop_mariadb |
stops it |
retry_setup |
runs setup again after a failure |
get_logs |
returns the last tail lines, 100 by default |
Credit Account
Two methods, both admin-only.
| Method | Arguments | Does |
|---|---|---|
post_adjustment |
credits, reason |
moves a balance by hand in either direction, with the reason on the row |
post_refund |
order, credits, reason |
gives credits back against the Razorpay order that granted them |
post_adjustment is how a balance is set without a payment. It is the path the
documentation screenshots used, because the gateway keys are empty on this host.
The two guest endpoints
Two functions carry allow_guest=True. They are the whole surface an
unauthenticated caller can reach, and both are rate limited to the same shape.
| Endpoint | Arguments | Rate limit | Checks |
|---|---|---|---|
benchpress.waitlist.join |
email, full_name, company, use_case |
3 an hour for each email and each IP address | none, by design |
benchpress.signup.sign_up |
email, full_name, redirect_to |
3 an hour for each email and each IP address | waitlist_open and the blocked domain list |
waitlist.join always answers the same way. A caller cannot tell a new address
from one already on the list, so the endpoint is not an account oracle.
signup.sign_up answers exactly as Frappe's own signup does, behind two gates
from Credit Settings. Both gates are off on a self-hosted install. See
Self-serve signup.
Two more waitlist functions are admin-only and not guest-reachable.
| Endpoint | Arguments | Does | Checks |
|---|---|---|---|
benchpress.waitlist.approve |
entries |
approves the selected entries and invites each one | require_admin |
benchpress.waitlist.notify_of_signup |
— | tells everybody still waiting that signup is live, once each | require_admin |
The whole list, counted
Fifty @frappe.whitelist() functions. Every one is in a table above.
| Location | Count |
|---|---|
benchpress/api.py |
35 |
benchpress/benchpress/doctype/database_server/database_server.py |
5 |
benchpress/benchpress/doctype/bench_instance/bench_instance.py |
4 |
benchpress/waitlist.py |
3 |
benchpress/benchpress/doctype/credit_account/credit_account.py |
2 |
benchpress/signup.py |
1 |
Recount after a change:
grep -rn "@frappe.whitelist" --include=*.py benchpress/ | wc -l
Adding an endpoint
- Write the function in the module that owns the concern, not in
api.py. - Add a thin wrapper in
api.pythat calls a permission helper first. - If the action costs credits or hits a cap, add
requires_admissionunder
@frappe.whitelist(). - Query with
frappe.qb. A rawfrappe.db.sqlstring is a lint failure. - Add the endpoint to the table on this page.
The decorator order matters. @frappe.whitelist() goes on top, and
@requires_admission(...) below it, so the gate runs inside the published
function.
Verify
Confirm an endpoint refuses the caller it should refuse:
bench --site <site> run-tests --app benchpress --module benchpress.tests.test_api_authorization
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
PermissionError from every endpoint |
The user holds none of the three roles | Grant BenchPress User. See Users and roles |
| An admin-only screen is blank | The API refused, and the router only redirects | Grant BenchPress Admin |
| A password field comes back empty | Frappe hides Password fields in a document read |
Call get_bench_credentials |
| Every deploy is refused | enable_credits is 1 and the caller cannot pay the hold |
Set it back to 0, or fund the account |
/credits redirects to Labs |
Credits are off, so the screen does not render | Expected. See Credits and billing |
| A build endpoint returns at once | It queues the work | Read the Build Log |
| A second deploy click does nothing | The job id deduplicates | Expected. Watch the running deploy |
Related
- Data model — the rows these endpoints read.
- Deploy pipeline — what
create_benchsets running. - Production safety — the endpoint-by-endpoint audit of these guards.
- Realtime — how a screen hears about the result.