MongoDB access governance — every command reviewed before it runs
Last updated
AccessFlow governs MongoDB through the same pipeline as a SQL database: a submitted command is parsed, classified, risk-scored by the AI analyzer, routed to an approver, and only then executed — with row-level security injected into the filter and fields masked on the way out. MongoDB is not JDBC, so it runs as a signed engine plugin the catalog resolves on demand.
- Family
- Document
- Query language
- Mongo shell / JSON command
- Runs as
- Engine plugin (SHA-256 pinned)
- Default port
27017- Default SSL mode
REQUIRE- Install
- One-click from the connector catalog
How AccessFlow connects
MongoDB is an engine plugin, not an in-process JDBC datasource: a standalone shaded JAR, pinned in the connector catalog by URL and SHA-256, downloaded on first use, hash-verified, and loaded into an isolated classloader. If the hash does not match, it does not load. In an air-gapped install the JAR can be pre-seeded into the driver cache and the runtime told never to reach the network.
The plugin caches one native MongoClient per datasource and the driver pools connections internally. The connection string is built from host, port (27017 by default), database, credentials and SSL mode. A datasource with a read replica configured routes reads with secondaryPreferred.
What AccessFlow understands
Both MongoDB query forms are accepted, auto-detected by their leading token. The shell form covers db.users.find({…}).limit(10), aggregate, the insert, update, replace and delete families, createIndex, createCollection, drop, distinct and countDocuments. The JSON command form is the native command document — { "find": "users", "filter": {…} } and its equivalents.
Arguments are read by a relaxed JSON parser that tolerates single quotes, unquoted keys, comments and trailing commas, and falls back to MongoDB's own lenient reader so shell constructors like ObjectId(…), ISODate(…) and NumberDecimal(…) parse too — which matters because AI-drafted insertMany statements are full of them.
Each operation maps onto the same query types the SQL engines use: find, aggregate, count and distinct are reads; the insert family is INSERT; update, replace and findAndModify are UPDATE; the delete family is DELETE; collection and index management is DDL. So the permission model, routing policies and approval chains apply completely unchanged.
What it refuses
Server-side JavaScript and write-exfiltration operators are rejected with HTTP 422 before execution: $where, $function, $accumulator, $out and $merge. The first three run arbitrary code inside the database; the last two write query output to another collection, which would route data around the audit trail entirely.
The check runs against the parsed command tree, not the submitted text, so an operator nested deep inside an aggregation pipeline is found just as reliably as one at the top level. Any operation the parser does not recognise is refused rather than passed through.
Row-level security and masking
A row-security policy becomes a filter fragment merged into the find, update or delete filter, or prepended to an aggregation pipeline as a $match stage. An INSERT into a collection carrying a policy is rejected outright — a write cannot be filtered into existence. A policy with an empty value list is a deny-all rather than a no-op, so a misconfigured policy denies rather than exposes.
Result documents are flattened into the same tabular result shape the SQL engines produce — columns are the ordered union of top-level fields, with nested objects and arrays preserved — and masks are applied per value by the same masker every other engine uses. BSON scalars are normalised on the way out: an ObjectId becomes hex, a Decimal128 a decimal, a date an ISO string, binary base64.
What the rewrite actually does
A policy restricting the orders collection to the caller's region is merged into the filter — and for an aggregation, prepended as a $match stage so it runs before anything else in the pipeline:
// submitted
db.orders.find({ status: "OPEN" })
// executed (row-security policy: orders.region = the caller's region)
db.orders.find({ status: "OPEN", region: "EU" })
// submitted (aggregation)
db.orders.aggregate([{ $group: { _id: "$status", n: { $sum: 1 } } }])
// executed — the policy becomes the first stage
db.orders.aggregate([{ $match: { region: "EU" } },
{ $group: { _id: "$status", n: { $sum: 1 } } }])
Prepending rather than appending is the point: a $match after a $group would filter the aggregate rather than the rows it was computed from, which is a different — and wrong — answer.
Introspection, dry-run and cost estimates
MongoDB has no fixed schema, so introspection samples documents to infer it: databases become schemas, collections become tables, observed fields become columns, and _id is flagged as the primary key. That view feeds the ER diagram, editor autocomplete and the AI analyzer's prompt exactly as a relational schema would.
A dry-run runs the command through MongoDB's own explain and returns the query planner's output without executing. On submission, an UPDATE or DELETE additionally gets an exact affected-document count via countDocuments, with the row-security filter applied — so a reviewer sees the real blast radius, not an estimate of the unfiltered statement.
Text-to-query
Natural-language drafting is offered for MongoDB datasources. The generation prompt is engine-aware, so it drafts a shell command or its JSON command form rather than SQL — and the draft is submitted through the normal pipeline, with no shortcut around parsing, risk analysis or approval.