Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ A keyless node reading a local inbox, serving the bundled single-variant COVID
allele-frequency sample. Nothing to author, no keys, no Docker. First the node:

```bash
mkdir -p ~/gdi-demo/inbox ~/gdi-demo/datasets
mkdir -p -m 0700 ~/gdi-demo/inbox ~/gdi-demo/datasets # keep the inbox owner-only, or the node warns
cat > ~/gdi-demo/node.toml <<EOF
[service]
base_url = "http://localhost:8080"
Expand Down
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ disallowed-methods = [
{ path = "gdi_node_standalone_core::util::write_durable_atomic", reason = "picks the file mode, and the default is 0644, so every call must state whether the bytes are secret. Use write_durable_atomic_private (0600) for anything that may carry key material or credentials, and #[expect(clippy::disallowed_methods, reason = \"not secret: ...\")] to keep this one" },
{ path = "tokio::fs::write", reason = "the async sibling of the std::fs::write ban above: neither atomic nor durable, and 0644. Use core::util::write_durable_atomic (or write_durable_atomic_private) from spawn_blocking, and #[expect(clippy::disallowed_methods, reason = \"...\")] where the bytes are ciphertext streamed to a scratch path" },
{ path = "tokio::fs::File::create", reason = "the async sibling of the std::fs::File::create ban above: neither atomic nor durable, and 0644. Same remedy" },
{ path = "std::fs::create_dir_all", reason = "creates every component at umask default (0755): use core::util::create_private_dir (0700) for anything on the node's data path. Keep it with #[expect(clippy::disallowed_methods, reason = \"...\")] stating why umask default is right, such as an operator-chosen path on the operator's own machine (the dataset tool), or the provider-writable inbox ingress" },
{ path = "std::fs::create_dir_all", reason = "creates every component at umask default (0755): use core::util::create_private_dir (0700) for anything on the node's data path. Keep it with #[expect(clippy::disallowed_methods, reason = \"...\")] stating why umask default is right, such as an operator-chosen path on the operator's own machine (the dataset tool)" },
{ path = "tokio::fs::create_dir_all", reason = "the async sibling of the std::fs::create_dir_all ban above: umask default. Same remedy, from spawn_blocking" },
{ path = "std::fs::create_dir", reason = "the non-recursive sibling of the std::fs::create_dir_all ban above: umask default (0755). Use core::util::create_private_dir (0700); keep it with #[expect(clippy::disallowed_methods, reason = \"...\")] where umask default is right, as for create_dir_all" },
{ path = "tokio::fs::create_dir", reason = "the async sibling of the std::fs::create_dir ban above: umask default. Same remedy, from spawn_blocking" },
Expand Down
5 changes: 2 additions & 3 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,8 @@ into `target/conformance/union.ttl`. It then runs:
skip. A value constraint is reported and skipped, for one of two reasons the output
distinguishes:

- the property is optional and the corpus does not populate it (`dct:type`,
`Distribution`'s `adms:status`). Closable by extending the corpus, at the cost of a
fixture less like a real node.
- the property is optional and the corpus does not populate it (`dct:type`). Closable
by extending the corpus, at the cost of a fixture less like a real node.
- the site is structurally unexercisable. Every remaining `sh:uniqueLang` skip is this:
the node emits those paths as plain literals because the model behind them is a string,
not a language map (`Agent.name` and `OtherIdentifier.name` for `foaf:name`, the
Expand Down
12 changes: 6 additions & 6 deletions conformance/check_fdp_negative.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,19 +866,19 @@ def main(argv: list[str]) -> int:
return 1
# Value-constraint skips are reported, not fatal: unlike the mandatory predicates above,
# an `sh:in`/`sh:pattern` site can legitimately target an optional property the corpus
# does not populate (`adms:status`, `dct:type` on a non-synthetic dataset). Making these
# fatal would force the corpus to carry every optional field, giving a fixture less like
# the real node the crawl is meant to represent.
# does not populate (`dct:type` on a non-synthetic dataset). Making these fatal would
# force the corpus to carry every optional field, giving a fixture less like the real
# node the crawl is meant to represent.
if value_skipped:
print(
f"\nnote: {len(value_skipped)} value-constraint site(s) not exercised by this "
"union, skipped. There are two reasons a site lands here, and only one of them "
"is a corpus gap:"
)
print(
" (a) optional property the corpus does not populate — `dct:type` (set only on "
"synthetic datasets) and `Distribution`'s `adms:status`. Closable by extending "
"the fixture corpus, at the cost of a corpus less like a real node."
" (a) optional property the corpus does not populate — `dct:type`, set only on "
"synthetic datasets. Closable by extending the fixture corpus, at the cost of a "
"corpus less like a real node."
)
print(
" (b) structurally unexercisable — every remaining `sh:uniqueLang` site. The "
Expand Down
8 changes: 7 additions & 1 deletion crates/fairdp/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ pub(crate) fn add_agent(
/// `dcatap:applicableLegislation` and `dct:license` are inherited from the parent
/// dataset. Its title and `dcat:accessURL` (where a user asks for access) come from
/// `[fairdp.distribution]`; the inline `dcat:accessService` `dcat:DataService` is the
/// aggregated Beacon.
/// aggregated Beacon. Its `adms:status` is constant, as on the dataset.
#[must_use]
pub fn distribution_graph(entry: &DatasetEntry, ctx: &FdpContext) -> Graph {
let mut b = GraphBuilder::new();
Expand Down Expand Up @@ -374,6 +374,12 @@ pub fn distribution_graph(entry: &DatasetEntry, ctx: &FdpContext) -> Graph {
}
b.add_iri(subj.clone(), vocab::DCT_LICENSE, &entry.metadata.license);

b.add_iri(
subj.clone(),
vocab::ADMS_STATUS,
vocab::DISTRIBUTION_STATUS_COMPLETED,
);

// Inline DataService blank node under dcat:accessService.
let service_subj =
b.add_blank_node(&subj, vocab::DCAT_ACCESS_SERVICE, vocab::DCAT_DATA_SERVICE);
Expand Down
6 changes: 6 additions & 0 deletions crates/fairdp/src/vocab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ pub(crate) const FDP_SPEC_V1_2: &str = "https://specs.fairdatapoint.org/v1.2/fdp
pub(crate) const DATASET_STATUS_COMPLETED: &str =
"http://publications.europa.eu/resource/authority/dataset-status/COMPLETED";

/// `adms:status` constant on a distribution: the EU distribution-status `COMPLETED`
/// value. Distributions have their own status table, the only one gdi-metadata's
/// `DistributionShape` accepts.
pub(crate) const DISTRIBUTION_STATUS_COMPLETED: &str =
"http://publications.europa.eu/resource/authority/distribution-status/COMPLETED";

// --- XSD datatype IRIs ---

/// `xsd:string`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ expression: nt
<https://gdi-ee.example.org/fairdp/distribution/GDI-EE-UTARTU-20260409143052837> <http://purl.org/dc/terms/license> <https://creativecommons.org/licenses/by/4.0/>
<https://gdi-ee.example.org/fairdp/distribution/GDI-EE-UTARTU-20260409143052837> <http://purl.org/dc/terms/title> "GDI User Portal"
<https://gdi-ee.example.org/fairdp/distribution/GDI-EE-UTARTU-20260409143052837> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/dcat#Distribution>
<https://gdi-ee.example.org/fairdp/distribution/GDI-EE-UTARTU-20260409143052837> <http://www.w3.org/ns/adms#status> <http://publications.europa.eu/resource/authority/distribution-status/COMPLETED>
<https://gdi-ee.example.org/fairdp/distribution/GDI-EE-UTARTU-20260409143052837> <http://www.w3.org/ns/dcat#accessService> _:c14n0
<https://gdi-ee.example.org/fairdp/distribution/GDI-EE-UTARTU-20260409143052837> <http://www.w3.org/ns/dcat#accessURL> <https://portal.example.org/allele-frequency>
<https://gdi-ee.example.org/fairdp/distribution/GDI-EE-UTARTU-20260409143052837> <http://www.w3.org/ns/dcat#mediaType> <https://www.iana.org/assignments/media-types/application/json>
Expand Down
2 changes: 1 addition & 1 deletion crates/gdi-dataset-tool/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ pub fn require_inbox(inbox: &Path) -> Result<(), ToolError> {
"inbox {} does not exist. The node creates its inbox at startup, so an absent \
one means this is not that node's inbox path. Check the profile's `inbox` \
or `--inbox` against the node's `[service].inbox`. If you are staging a drop \
before the node's first start, create it first: mkdir -p {}",
before the node's first start, create it first: mkdir -p -m 0700 {}",
inbox.display(),
inbox.display()
))),
Expand Down
11 changes: 3 additions & 8 deletions crates/gdi-node-standalone/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1171,14 +1171,9 @@ async fn run(cli: Cli) -> Result<()> {

// Inbox watcher + periodic rescan, if an inbox is configured.
if let Some(inbox) = state.config.service.inbox.clone() {
// The inbox is an operator-provisioned ingress a provider drops into, so it keeps
// its configured permissions. Unlike the rest of the data path, it is not forced
// 0o700.
#[expect(
clippy::disallowed_methods,
reason = "the inbox is an operator-provisioned ingress that keeps its configured permissions"
)]
std::fs::create_dir_all(&inbox)
// A pre-existing inbox, such as a mounted volume or one the operator made, keeps its
// mode. One the node creates is owner-only, like the rest of the data path.
gdi_node_standalone_core::util::create_private_dir(&inbox)
.with_context(|| format!("creating inbox {}", inbox.display()))?;
// An inbox writable by more than its owner is a multi-tenant plaintext inbox, an
// unsupported topology: a co-tenant can swap a just-validated staging file for a
Expand Down
10 changes: 4 additions & 6 deletions deploy/kubernetes/base/networkpolicy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@ spec:
protocol: TCP
# Management plane: the monitoring namespace's scraper only.
#
# `namespaceSelector` and `podSelector` sit in the same `from` element: one
# element ANDs its selectors, whereas two elements OR them — which would widen this
# rule to every pod in the cluster carrying the label, the exact opposite of the
# intent. A bare `namespaceSelector` would admit every pod in `monitoring` (Grafana,
# Alertmanager, any sidecar, anything that later lands there) to the unauthenticated
# dataset-state oracle, so it is worth the extra selector.
# `namespaceSelector` and `podSelector` sit in the same `from` element, so both
# must match. Split into two elements, either would do, and every pod in
# `monitoring` (Grafana, Alertmanager, anything that later lands there) would
# reach the unauthenticated dataset-state oracle.
#
# Adjust the pod label to match your scraper — `app.kubernetes.io/name: prometheus` is
# the prometheus-operator convention, not a universal one. Getting it wrong blocks the
Expand Down
4 changes: 2 additions & 2 deletions docs/gdi-dataset-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -1830,8 +1830,8 @@ asynchronously: a successful `deploy` only drops the artifact into the inbox, so
> wrong, and `deploy`, `publish`, `unpublish` and `delete` all refuse rather than create
> it. A created-on-demand inbox would turn the one signal a mistyped path gives, `ENOENT`,
> into a phantom inbox that accepts every drop while the node reads the real one and sees
> nothing. If you are staging a drop before the node's first start, `mkdir -p <inbox>`
> first; the error message names that command.
> nothing. If you are staging a drop before the node's first start, run
> `mkdir -p -m 0700 <inbox>` first; the error message names that command.

```bash
gdi-dataset-tool deploy GDI-EE-EXAMPLE-20260409143052837.tar.c4gh --inbox /var/inbox
Expand Down
Loading