Europass becomes an internal plugin #99
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
Reference
Postulo/postulo#99
Loading…
Add table
Add a link
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?
Observation
This is the part of that request which fits the architecture as it stands, and improves it.
Why it is a good idea
resume/europass.pyalready has the shape of a plugin without being one:readdecides between the two formats on the first character, which is exactly thecan_handle/parsesplit sources already use. And Europass is not the last CV formatanybody will want: JSON Resume, HR-XML, a LinkedIn export, a plain PDF. Each is somebody
else's itch, which is the argument the plugin system was built on --
registry.pyputs itas "the person who cares about a particular job board... should not have to wait for this
project to accept a patch".
What is missing
There is no importer kind. The four are
source,notifier,store,sync, and animporter is none of them: a source reads a job posting off a page, an importer reads a
person's career out of a file. Different input, different output, different failure mode.
So this issue is mostly the new kind:
postulo.importersas an entry-point group, added toGROUPS.ImporterPluginprotocol:can_handle(data, filename) -> bool,read(data) -> Record,and the identity fields from #97.
worth deciding: they share
apply()and aRecord, andread()already sniffs betweenthem, so one importer with two formats is closer to the truth than two importers. The
request says "XML and JSON" as two; the code says one thing that reads both. Suggest one,
named for the format family, saying which dialect it found -- which
Recordalready does.Two things an importer must not inherit from a source
A source is given a URL and HTML. An importer is given a file somebody uploaded, which
is a different threat.
europass.pyrefuses a DOCTYPE before parsing -- "a DOCTYPE iswhere entity expansion lives, and the point is to refuse it rather than to hand it to a
parser and hope" -- and caps the file at
MAX_BYTES. Those refusals belong to the kind,enforced by Postulo before a plugin sees a byte, not left to each plugin to remember. A
third-party importer that forgets the DOCTYPE check would be an XXE hole in an application
holding people's CVs.
A source's output is reviewed by a person before anything is saved;
base.pyis explicitthat a parser guessing wrong should "waste a few seconds of somebody's attention, not put a
fabricated job title into their records". An import writes a career record. The same rule
has to hold, and
EuropassImportViewalready works that way -- the preview is not a nicetyto be dropped when the reader becomes a plugin.
Scope
apply()in core -- an importer produces aRecord; itdoes not write to the database. That boundary is what keeps a third-party importer from
needing ownership scoping of its own.
tests/security/: an importer never bypasses the preview; a hostile file is refused by thekind even when the plugin would have accepted it.
docs/PLUGINS.mdgains the kind.Classification
Enhancement. Depends on #97 for the identity fields, and on nothing else.
Narrowed
Right, and the original was doing two things at once. One internal Europass importer,
registered the way the other built-ins already are. The third-party surface is deferred —
filed on 0.4.0 so it is not lost.
Also settling the one-or-two question the issue raised, in the same direction: one
importer that reads both formats, because that is what the code already is.
read()sniffs the first character and dispatches;
read_xmlandread_jsonreturn the sameRecord;apply()is shared. Two plugins would be one thing described twice.What it actually costs, using the machinery that is already there
register_builtinis howEmailNotifierandLocalStoreare registered — five lines in anAppConfig.ready():and built-ins skip the protocol check entirely —
plugins()instantiates them directly:So the whole of it is: one entry in
GROUPS, a class wrapping theread()that exists, oneregister_builtincall, the identity fields from #97, and the import view asking theregistry rather than naming Europass. No protocol contract to get right, no
docs/PLUGINS.mdchapter, no third-party security review.One thing that should still be done, and it is a move rather than a build
GROUPSmaps a kind to an entry-point group, so addingimporternamespostulo.importerswhether or not anybody is told about it. Nothing publishes there, so itreturns nothing and costs nothing — but the door is ajar rather than shut, and a third-party
importer that loaded would not have
europass.py's protections:So move the DOCTYPE and size refusals into the kind now. It is relocating code that
already exists and is already correct, one level up, and it is cheaper than the alternative
(a branch in
plugins()to keep built-ins off the entry-point path). With that done the opendoor is harmless, and the 0.4.0 issue becomes documentation rather than security work.
Unchanged
The preview stays.
EuropassImportViewshows what was read before anything is written, andan import writes a career record —
base.py's rule that a plugin should "waste a fewseconds of somebody's attention, not put a fabricated job title into their records" applies
at least as strongly here.
apply()stays in core: an importer produces aRecordand doesnot touch the database, which is what keeps an importer from needing ownership scoping of its
own.
An importer is a kind of plugin, and Europass is the first twoto Europass becomes an internal pluginLogos are now asked for as well — filed as #106, which covers where a plugin logo
lives, how it is served under
img-src 'self', and the format questionjobs/logos.pyalready answered once for company logos.
The Europass logo specifically needs a decision rather than a download: it is a European
Union trademark, and AGPL grants rights to code and cannot sublicense somebody else's mark.
That is in #106 with the options. Not blocking this issue — a plugin with no logo has to
render properly regardless, so the importer can land before the artwork does.
Done in
df99ca9, and the dependency on #97 removed rather than waited on: this was built with the identity fields that exist today —name,version,kind,label,description— and #98 sweeps every built-in when #97 lands, this one included.Worth recording that Forgejo refused the
Closes #99in the commit because of that dependency. An issue with an unresolved dependency cannot be closed by a commit, so a speculative dependency does not merely describe an order, it silently prevents the issue from ever closing itself.