Publications: suggest an entry from a DOI or from your ORCID record, only when asked, through the guarded client #688
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#688
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Typing a paper's authors, venue, volume and pages by hand is the part of adding a publication nobody wants to do. The maintainer's list says suggestions "can be obtained using ORCID or DOI search". #687 gives publications a record; this issue fills one from a public registry, by a button a person presses, never on its own. It is the first thing in Postulo that asks a scholarly service anything, so the limits matter as much as the lookup.
What exists today
src/postulo/plugins/identifiers/schemes.py:95-135) is never sent anywhere ("Nothing here asks ORCID whether an identifier exists",src/postulo/accounts/identifiers.py:21-23). The nearest network action on the career is one request per press (src/postulo/resume/links.py:35-86: 8 s, 3 redirects, "Postulo never fetches a link on its own").postulo.plugins.http.public_only_client(src/postulo/plugins/http.py:206-222) refuses a non-public address on every request, redirects included, and pins the connection to the address it approved;until(deadline_in(n))cuts every wait to a deadline;read_body(response, limit=, deadline=)(:342) stops at a byte and a time limit and unpacks only gzip and deflate (ACCEPT_ENCODING,:35). Its defaultUser-Agentnames the project, not the operator (src/postulo/plugins/public_addresses.py:29). The wiki calls it the client "for what is public by definition" (Writing-a-plugin.md:703);tests/security/test_outbound.pyshows how it is tested (stubbed resolver,MockTransport).core/throttle.py(consume,rate_for;POSTULO_CAPTURE_RATE=30/h,config/settings/base.py:411); per #366, link checks and logo fetches spend none. An operator address for third parties exists once: web push writes "the operator, never the project" (src/postulo/plugins/browser/webpush.py:107-116;DEFAULT_FROM_EMAIL, defaultpostulo@localhost,config/settings/base.py:708). A per-person switch for a capability is a feature plugin (src/postulo/plugins/phone_numbers/__init__.py,plugins.policy.decide).What the services say, read on 2026-10-03.
Acceptheader onhttps://doi.org/<doi>returns one record in a chosen format (CSL JSON, BibTeX, RIS…) whichever agency registered it (Crossref, DataCite, mEDRA); "the mapping is not always direct". Observed: a Crossref DOI redirects once toapi.crossref.org/v1/works/<doi>/transform, a DataCite DOI todata.crosscite.org/<doi>. A Crossref record was 21 KB, nearly all of it thereferencelist and abstract; a many-author physics paper's was 682,304 bytes with 5,156 authors. The returnedURLishttp://dx.doi.org/.... Crossref's owntype(journal-article) came back, not CSL'sarticle-journal; DataCite's was CSL (dataset): both vocabularies must be read.mailtoparameter oragentheader; Plus, a key. Limits, for a single record: public 5 requests a second and one concurrent, polite 10 and three, Plus 150 (a list query is 1 and 3 a second from 1 December 2025, per Crossref's announcement of the change; the reply saidx-rate-limit-limit: 5,x-rate-limit-interval: 1s,x-api-pool: public-single); 429 when exceeded, 403 for a manual block. They ask for caching and an identifyingUser-Agent. An anonymous request can always be tied to an IP address, kept 90 days; contact details given in a request stay in the logs 90 days and are used only to contact whoever causes problems (rest-api-doc,api_tips.md). On reuse, the REST API page says almost none of the metadata is subject to copyright and may be used for any purpose, but some abstracts may be subject to copyright by publishers or authors.User-Agentormailto=) 1000, authenticated 3000; doi.org content negotiation 1000; 429 when exceeded. Metadata is CC0 (its data-file policy and POSI commitment).GET https://pub.orcid.org/v3.0/<orcid>/worksanswered 200 JSON with no credentials) and the Public (registered) API. Limits: anonymous 12 requests a second, 40 burst, 25,000 reads a day per IP; registered 100,000 a day per client; over the burst is 503. Terms (Public APIs Terms of Service, updated 23 October 2024, which define the Anonymous API and the Public API together as the “Public APIs”): non-commercial ("may not charge any re-use fees… or make use of the public APIs in connection with any revenue-generating product or service"); credentials are "granted to individuals and not organizations", personal, not shareable; no continuous polling. A works summary has title, type, date, journal title and external ids (DOIs), and no authors.Checked against
mainatf09b6010b.Proposal
resume/lookups.py):from_doi(doi), one request tohttps://doi.org/<doi>for CSL JSON (2 MiB, 10 s in all), andfrom_orcid(orcid), one request tohttps://pub.orcid.org/v3.0/<orcid>/worksasking for JSON (Accept: application/json; the default answer is XML; 4 MiB, 10 s). Both throughpublic_only_clientinsideuntil(...)withread_body. Hosts are fixed; the only input in a URL is a DOI or an ORCID that passed its own check, percent-encoded into the path; at most 3 redirects, each approved by the guard.<i>and MathML) and of control and bidirectional-override characters, and drawn escaped. The returnedURLis ignored. An unknown type becomesmisc, never a refusal; the type table accepts Crossref's and CSL's names.POSTULO_LOOKUP_RATE(default20/h), throughcore/throttle.consume, spent even when the service fails, as capture's is.publication-lookups, on by default; off hides both buttons and makes both endpoints answer as if absent. A row per destination in Hardening's table (what triggers it, whose address, which guard, which switch).Decisions
POSTULO_DEFAULT_FROM_EMAILis real and the operator opts in. A person's own email never leaves.Prior art
habanero(Crossref client): MIT; 2.9.2, 2026-06-17; needshttpx2(BSD-3-Clause; OSV lists five advisories, two High, all fixed by 2.12.0),tqdm,urllib3; no advisory for habanero; not a dependency. Rejected: it opens its own connections, so the guard, pinning and body limits would not apply (docs/THREAT-MODEL.mdrule 5), for what is two GETs.pyorcid: MIT; 1.2.2, 2025-11-04;requests,python-dotenv,xmltodict; no advisory.orcid(python-orcid): BSD; 1.0.3, last released 2018-07-17.crossrefapi: 1.7.0, 2025-07-03;requestsandipython; licence not stated in its PyPI metadata (not verified); no advisory. All rejected, same reason. Every licence stated is MIT or BSD, so compatible with AGPL-3.0; the reason is the guard.applications/ical.pyanddocuments/docx.pyare hand-written file formats and not the precedent here.Builds on
#687 (the record), #366 (the outbound inventory), #46 (the person's ORCID),
docs/THREAT-MODEL.mdrule 5.Needed by
Nothing. A DOI scheme in the identifiers registry (a work's, not a person's) is a separate idea.
Done when
tests/test_publication_lookup.py, every response from aMockTransportand the resolver stubbed: Crossref- and DataCite-shaped records fill blanks and leave typed values; both type vocabularies map and an unknown becomesmisc; markup and control characters go; 5,000 authors become 100 and "and others"; abstract and references are never kept; the ORCID review marks held works, ticks none, adds only what is ticked, capped at 20; a person with no ORCID is not offered the button and a posted ORCID is ignored.tests/security/test_outbound.pygains: a doi.org answer redirecting to a private address is refused, and the page says nothing of it; an oversize or trickling answer stops at its limit and deadline; the DOI is only in the path of a fixed host.tests/security/test_rate_limits.pygains the allowance (spent on failure, per account).publication-lookupshides both buttons and both endpoints refuse (tests/test_plugin_policy.py, extended); new pages jointests/test_page_coverage.py(asresume:link_check,:182-183) andtests/security/test_isolation_sweep.py.tests/test_wiki_surface.pyuntouched. New strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349); the wiki and Hardening row in the same commit.From the list of improvements of 2026-10-02.