The challenge
A US commercial bank wanted Microsoft 365 Copilot to answer over a Cloudera data lake. Apache Ranger decides who may see what in that lake, and it is expressive in ways a search index is not: it filters rows per user, masks columns per user, denies a resource outright, scopes policies to a security zone, and switches policies on and off by the clock.
A connector writing into a search index has one instrument: a static list of principals, attached to an item, at the moment it is written. No clock, no row filter, and in the push API no way to express a deny.
The question was never how to translate Ranger into a search index. It was which parts cannot be translated, and what the connector must do when it meets one. Get that wrong and the failure is silent: the crawl succeeds, the index looks complete, and somewhere in it sits a document one person was never entitled to see.
The solution
Nineteen numbered controls, each with the code that implements it and the test that pins it. Not a policy document but a mapping an auditor can check line by line, where every claim points at a named test that fails if the behaviour changes.
Some things are never indexed. A table whose rows Ranger filters or whose columns it masks stays out, because an index holds one copy: it would either leak the unfiltered rows to everyone granted the item, or store the masked version and lie to those entitled to the real one.
A deny is obeyed by refusing, never by mirroring. A mirrored deny protects only while the translation is right every time, and a drifted translation fails open. The permission type used here cannot express a deny at all, so the property is structural rather than conventional.
One trap only showed up under scrutiny. Ranger folds case on every resource, which is right for Hive names and wrong for paths: HDFS is case-sensitive, so /data/Finance and /data/finance are two directories, holding different files under different permissions, that folding would treat as one.
Anything depending on the clock stops the run, because a Graph permission has nowhere to put a clock. Guards fire on over-granting only and under-granting is logged, since a guard that fires on the safe direction teaches operators to disable guards.
The results
The outcome worth stating is not a throughput figure. The connector currently refuses to crawl the bank’s QA cluster. A tag-service policy there carries a condition, so two controls fire and the run stops. That is the design working, with the blocker in code rather than in a document: a document is read once, a refusal every time.
This has never run against a production cluster. The evidence is structural: controls, implementations and tests. There is no crawl duration, no throughput and no production history.