Skip to content

Scenario bundle acronym uniqueness is not enforced, and the rejection answers HTTP 200 #2511

Description

@jh-RLI

1. The comparison compares two different things

factsheet/views.py:167-171:

for s, p, o in oekg.triples((None, RDF.type, OEO.OEO_00020227)):
    study_acronym = oekg.value(s, DC.acronym)
    if str(clean_name(acronym)) == str(study_acronym):
        Duplicate_study_factsheet = True

The left side is normalised, the right side is the raw stored value. clean_name
(factsheet/helper.py) replaces spaces, hyphens, %, Ö/ö, /, :, ( and ) with
underscores or expansions — but bundles are stored with dc:acronym set to the unmodified input.

So for an existing bundle with acronym NEP 2037, a second create with the same acronym compares
"NEP_2037" == "NEP 2037" → False, and the duplicate is created. The check only ever fires for
acronyms containing none of the replaced characters.

Effect: acronym uniqueness is silently unenforced for a large share of real acronyms.

The update path has its own copy of the same comparison (:625-634).

2. When it does fire, it answers 200

response = JsonResponse("Factsheet exists", safe=False, content_type="application/json")

No status code, so Django sends 200 OK. A rejected create is indistinguishable from a successful
one for any client that checks status codes rather than parsing the body string. It should be a
409 Conflict.

3. It is an O(bundles) graph scan per create

The check iterates every OEO_00020227 in the graph and issues an oekg.value() per bundle. With
SPARQLUpdateStore as transport (factsheet/oekg/connection.py) each of those is its own HTTP round
trip to Fuseki. A single ASK would answer the same question in one request.

4. Adjacent: the client-supplied uid is unvalidated

uid = request_body["uid"]
study_URI = URIRef("https://openenergyplatform.org/ontology/oekg/" + uid)

A client-supplied string is concatenated straight into the IRI namespace — no UUID check, no
character check. Note the create checks the acronym for duplicates but never the uid, so a
POST carrying an existing uid with a different acronym writes its triples into the existing
bundle's URI: a silent merge.

Suggested fix

  • Compare like with like — normalise both sides, or store the normalised form alongside and compare
    that. Decide which is canonical and write it down.
  • Answer 409 on a duplicate.
  • Replace the scan with a single ASK.
  • Mint the uuid server-side rather than validating a client-supplied one — that closes the
    surface instead of policing it.

This is already built and merged for the REST API: oekg/acronyms.py (#2471) holds the whole
acronym promise, and the server mints the uid (#2477). The fix here is to route the browser's create
through the same code rather than to write a second answer.

Why it matters beyond the browser

The API depends on acronym uniqueness being real: a stateless modelling pipeline re-identifies its
bundle with GET /api/v0/scenario-bundles/?acronym=…, so an unenforced acronym makes re-import pick
the wrong bundle or create duplicates. Academy notebook #2464 teaches exactly that pattern.


Found while charting the OEKG REST API (WF-10, "Idempotent re-import for the modelling pipeline").
Verified still present on develop (f21a6a3b7) on 2026-09-21; line numbers are from that commit.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions