Repository navigation
feat(endpoints): access modes, named bot checks, forms (#2259 #2176) - #2330
Merged
Merged
Conversation
Changes manage_script endpoint_set, /endpoints/{name}, /botcheck/challenge, /api/v1/admin/bot-checks (replacing /api/v1/admin/settings/bot-check), /api/v1/admin/endpoints, /api/v1/portal/scripts and the script graph route, with migration 000221, and fixes 3 bugs found on the way.
Endpoints have one access mode each: authenticated (a platform API key or browser session among the binding's callers, the administrator always admitted), public (no check) or bot_check (a named check, on its checked_methods). Bot checks are a list of named checks; one an endpoint names cannot be deleted. Migration 000221 rewrites signed_in to authenticated with callers ["*"] and public to bot_check naming "default".
A bound script defining form_spec(req) and form_submit(req, values) is a form: GET answers the spec signed into a form_token, and a submission is checked against that spec (fields, types, limits, honeypot, minimum fill time) before form_submit runs.
The Automations list shows each script's endpoint and filters by endpoint=bound|unbound. The flow diagram draws handle(req), and a form's two handlers.
Bugs fixed:
1. script_endpoint_refusals' CHECK did not name the new caller reason, and a refused batch is retried whole, so one such count would have held back every endpoint's counts on a replica.
2. A form script was judged a library, and the unused-parameter lint refused a request handler's req.
3. Admin > Settings > Bot checks reset a new check's draft on a background refetch.
Closes #2259
Closes #2176
Closes #2314
Closes #2315
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2330 +/- ##
==========================================
- Coverage 94.79% 94.79% -0.01%
==========================================
Files 1417 1422 +5
Lines 115071 115639 +568
==========================================
+ Hits 109087 109624 +537
- Misses 5965 5995 +30
- Partials 19 20 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
manage_script endpoint_set,/endpoints/{name},/botcheck/challenge,/api/v1/admin/bot-checks(replacing/api/v1/admin/settings/bot-check),/api/v1/admin/endpoints,/api/v1/portal/scriptsand/api/v1/portal/scripts/{id}/versions/{version}/graph, adds migration000221, and fixes 3 bugs found while building it.Closes #2259
Closes #2176
Closes #2314
Closes #2315
Access modes (#2259)
Every endpoint has exactly one access mode, set on its binding (
internal/endpoint/epbinding):authenticatedcallersnames:*,persona:<name>,api_key:<name>. Empty admits no one but the administrator, who is always admittedpublicbot_checkchecked_methods(empty for every method; HEAD is checked where GET is)<name>signed_in,open_reads, the providernoneandallow_public_without_checkare gone, with no alias.manage_script's schema enumerates the three modes, and the portal route refusessigned_inwith 400.caller(internal/endpoint/eprefusal).middleware.UserInfo.APIKeyName, throughportal.User), soapi_key:<name>matches a key bound to a person as well as a standalone one. The persona comes from the registry's role resolution, and administrator status fromRegistry.AdminRule, the rule every other surface uses (internal/httpserver/endpointwire).Named bot checks
The deployment-wide
platform_settings.bot_checksection is replaced by thebot_checkstable (internal/botcheck/checks.go,checkstore.go). Each check has a name, a provider (pow,turnstile,hcaptcha) and that provider's settings.script_endpoints.bot_checkreferencesbot_checks(name)ON DELETE RESTRICT, so the database refuses deleting a check an endpoint names. The API answers 409 with those endpoints.GET /api/v1/admin/bot-checks(each check withused_byandusable, pluskey_available),GET|PUT|DELETE /api/v1/admin/bot-checks/{name}.Migration 000221
bot_checksand buildsdefaultfrom the old section. A section that was never written, or was set tonone, becomes proof of work at its defaults.signed_inbecomesauthenticatedwith callers{*}, which admits whom it admitted before.publicbinding that answered with no check becomespublic: one with open reads that answered only GET and HEAD, and every public binding where the section wasnonewith the override.publicbinding becomesbot_checknamingdefault, checking every method, or every method but GET and HEAD where its reads were open.nonewithout the override, those endpoints answered nothing; they becomebot_check: defaultand are turned off.callerto thescript_endpoint_refusals.reasonCHECK.On the dev database, which held bindings from earlier runs, the migration rewrote 158
signed_inbindings toauthenticated {*}and 76publicbindings tobot_check default.Forms (#2176)
A bound script that defines
form_spec(req)andform_submit(req, values), in place ofhandle(req), is a form (internal/endpoint/epform,internal/endpoint/eprun/form.go).form_spec(req). The platform checks the spec: field typestext,long_text,email,number,date,single_choice,multiple_choice,checkboxandhidden; unique names; choices only on choice fields; limits. It answers{spec, form_token, token_field, honeypot_field}.endpoint-form-v1). It is bound to the endpoint and valid for 24 hours, and no copy is kept, so a submission is checked on whichever replica receives it.websitehoneypot, or a submission sooner thanmin_fill_secondsafter the GET (at least 2; a spec may raise it);{"errors": {field: message}}for an undeclared field, a missing required one, a wrong type, a value past its limit, or a hidden field that differs from its prefill.form_submit(req, values)gets every declared field, typed. It returns None to accept with the spec's confirmation and redirect,{"errors": {...}}to refuse field by field, or{"confirmation", "redirect"}to accept with its own.scriptlib); the entry-point lint accepts the two handlers with their fixed parameters (scriptlint);epbinding.Callableaccepts a form; andmanage_script helpdocuments the contract with a worked example.Endpoints in the Automations list (#2314)
GET /api/v1/portal/scriptsrows carryendpoint(name, address, access mode, whether it is on), read in one call per page (scriptlist.Endpoints).endpoint=bound|unboundnarrows the listing in the store, ahead of the page cap. The list has an Endpoint column (the address, with Off when the binding is off) and an Endpoint filter (All, Bound, Not bound).Flow for endpoint scripts (#2315)
The flow walk used to add only the platform's call of
main(), so an endpoint script drew an empty graph. It now enters throughhandle(req)when there is nomain(), and throughform_spec(req)thenform_submit(req, values)for a form (internal/platform/scriptflow/survey.go). A script that definesmain()draws as before.Bugs fixed
000207's CHECK onscript_endpoint_refusals.reasondid not namecaller. A replica adds all its counts in one statement and puts a refused batch back to retry, so onecallercount would have held back every endpoint's refusal counts on that replica.000221adds the reason to the constraint, andeprefusal.Counternow drops any reasoneprefusal.Reasonsdoes not list.TestRealDB_EveryRefusalReasonIsStoredwrites a count under every listed reason.main()norhandle(), was judged a library, and theunused-parameterlint refusedform_submit(req, values)whenreqwent unread. The parameters ofhandle,form_specandform_submitare now the platform's to pass.Readers of this state
make state-readers-checklisted the changed API contracts. Readers updated inui/src:api/admin/types/botCheck.ts,api/admin/hooks/botCheck.ts,pages/settings/BotChecksSettingsCard.tsx(replacingBotCheckSettingsCard.tsx),settingsSections.ts,AdminSettingsPage.tsx, the MSW handlers inmocks/handlers/storage.ts, and the screenshot routeadmin-settings-bot-checks.callers,bot_check,bot_check_provider,checked_methods,open_readsremoved):api/admin/types/endpoints.ts,pages/scripts/endpoints/EndpointsTab.tsx,EndpointDetailPage.tsx,endpointLabels.ts,mocks/data/endpoints.ts.auth_mode,callers,bot_check,checked_methods):api/portal/hooks/scriptEndpoint.ts,pages/scripts/ScriptEndpointCard.tsx,mocks/handlers/scripts.ts.endpoint) andendpoint=:api/portal/hooks/scriptListFilter.ts,scripts.ts,pages/scripts/ScriptListing.tsx,ScriptRow.tsx,ScriptGrid.tsx.references.endpoints,unscoped.any_caller_endpoints,unscoped.anyone_endpoints,signed_in_endpointsremoved):api/admin/types/personaAccess.ts,pages/settings/persona/AccessSummary.tsx,mocks/handlers/personaAccess.ts.New import edges
internal/admin/endpointapi -> internal/botcheck: the endpoint list shows eachbot_checkendpoint's provider.internal/httpserver/scripthttp/scriptlist -> internal/endpoint/epbinding: the listing reads its rows' bindings.internal/endpoint/epbinding -> internal/endpoint/epform:Callableaccepts a form.internal/endpoint/eprun -> internal/endpoint/epformandinternal/endpoint/epwire -> internal/endpoint/epform: the runner serves forms, and the wiring derives their key.internal/endpoint/epform -> internal/platform/scriptdialect,internal/platform/starlarkconv,pkg/portal/shareguest: parsing the handlers, reading whatform_specreturns, and deriving the token key, asinternal/botcheckderives its key.internal/botcheck -> internal/platform/settingsjsonis removed.Acceptance
Run through the real surface on the local dev stack (two replicas behind the proxy, Trino connection
acme), with transcripts atbuild/<n>/acceptance.md:signed_inrefused. Callers by persona, by API key name,*and empty, with the administrator always admitted and a refused caller counted undercaller. Public versus bot_check. Two proof-of-work checks issuing at their own difficulty and refusing each other's answers. A Turnstile check and a proof-of-work check on one deployment. Deletion refused while a check is in use.checked_methods[POST]. No surface sayingsigned_in.customer_id. Field errors in both body forms. Another form's or an altered token refused.form_submit's own field error. A tokenform_specminted verifying inform_submit. The help text.TestIssue2171_APublicBindingNeedsABotCheckUntilTheOverridewas removed with the rule it tested.Cloudflare's published Turnstile test key verifies every token, so the suite shows a proof-of-work endpoint refusing a Turnstile token, and two Turnstile checks with different keys answering one token differently. It does not show a Turnstile endpoint refusing a proof-of-work answer; that rests on the provider refusing a token its key did not issue.
The migration's per-binding mappings, including the round trip, are covered by
TestMigration_*_RealDB(internal/botcheck) against PostgreSQL, andmake migrate-checkpassed on pg16 and pg17.