|
@@ -2772,10 +2772,17 @@ grpc::Status DatabaseGrpcImpl::CreateRelation(
|
|
|
} catch (const std::exception& e) {
|
|
} catch (const std::exception& e) {
|
|
|
// The declaration is already persisted and armed for future
|
|
// The declaration is already persisted and armed for future
|
|
|
// writes; failing to backfill existing rows must not roll that
|
|
// writes; failing to backfill existing rows must not roll that
|
|
|
- // back (the relation is still an improvement over nothing, and
|
|
|
|
|
- // `relations check` can find what the backfill missed). Loud,
|
|
|
|
|
- // because a skipped backfill means pre-existing children stay
|
|
|
|
|
- // invisible to enforcement until re-run.
|
|
|
|
|
|
|
+ // back (the relation is still an improvement over nothing - it
|
|
|
|
|
+ // will maintain postings correctly going forward). Loud, because
|
|
|
|
|
+ // build_relation_index is one write txn: on a throw it aborts
|
|
|
|
|
+ // uncommitted, so the sub-db is left NOT CREATED at all (an
|
|
|
|
|
+ // aborted CREATE does not persist), not partially populated.
|
|
|
|
|
+ // ⚠ That means `relations check` cannot find what this missed -
|
|
|
|
|
+ // check_relation_dangling treats a missing index sub-db as
|
|
|
|
|
+ // "nothing to report" (relation_index_exists() == false), not
|
|
|
|
|
+ // "everything dangling", by design (see its own comment). The
|
|
|
|
|
+ // only path that recovers from a fully-failed backfill today is
|
|
|
|
|
+ // the next restart's applyRelationDeclarations() self-heal.
|
|
|
spdlog::error("v2.11 relations: bootstrap scan failed for '{}': {}",
|
|
spdlog::error("v2.11 relations: bootstrap scan failed for '{}': {}",
|
|
|
r.name, e.what());
|
|
r.name, e.what());
|
|
|
}
|
|
}
|
|
@@ -2948,6 +2955,24 @@ grpc::Status DatabaseGrpcImpl::CheckRelation(
|
|
|
const pb::CheckRelationRequest* request,
|
|
const pb::CheckRelationRequest* request,
|
|
|
pb::CheckRelationResponse* response
|
|
pb::CheckRelationResponse* response
|
|
|
) {
|
|
) {
|
|
|
|
|
+ // Admin-gated BEFORE the lookup, matching every other relation
|
|
|
|
|
+ // RPC (CreateRelation/DropRelation/ListRelations/GetRelationInfo) —
|
|
|
|
|
+ // and unlike the first cut of this handler, which resolved the relation
|
|
|
|
|
+ // (to learn its `child` collection to gate on) before checking access.
|
|
|
|
|
+ // That made CheckRelation a distinguishable existence oracle: a
|
|
|
|
|
+ // nonexistent relation returned success=false with an explicit "does
|
|
|
|
|
+ // not exist" message, while an existing relation the caller could not
|
|
|
|
|
+ // read returned PERMISSION_DENIED — two shapes a caller could tell
|
|
|
|
|
+ // apart by probing, exactly what gate()'s own comment warns against
|
|
|
|
|
+ // ("would let a caller map the access model by probing"). Gating on
|
|
|
|
|
+ // `child` first would need the same resolve-before-gate order, so it
|
|
|
|
|
+ // can't fix this without deciding what an UNRESOLVABLE name gates on
|
|
|
|
|
+ // instead — admin-first sidesteps that entirely and matches every
|
|
|
|
|
+ // sibling relation RPC.
|
|
|
|
|
+ if (auto st = requireAnyAdmin(context, "CheckRelation"); !st.ok()) {
|
|
|
|
|
+ return st;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
auto r = relation_manager_.getRelation(request->name());
|
|
auto r = relation_manager_.getRelation(request->name());
|
|
|
if (!r) {
|
|
if (!r) {
|
|
|
response->set_success(false);
|
|
response->set_success(false);
|
|
@@ -2955,15 +2980,6 @@ grpc::Status DatabaseGrpcImpl::CheckRelation(
|
|
|
return grpc::Status::OK;
|
|
return grpc::Status::OK;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Gated as an ordinary per-collection READ on the CHILD collection, not
|
|
|
|
|
- // admin - same reasoning as DescribeDelete: this changes nothing, but
|
|
|
|
|
- // the dangling parent ids and sample child ids it reports are facts
|
|
|
|
|
- // about data in `child`.
|
|
|
|
|
- smartbotic::database::Decision dec;
|
|
|
|
|
- if (auto st = gate(context, r->child, smartbotic::database::Access::Read, dec); !st.ok()) {
|
|
|
|
|
- return st;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
const auto rn = smartbotic::database::resolveCollection(r->name);
|
|
const auto rn = smartbotic::database::resolveCollection(r->name);
|
|
|
const auto rp = smartbotic::database::resolveCollection(r->parent);
|
|
const auto rp = smartbotic::database::resolveCollection(r->parent);
|