Skip to content

Commit 48b8864

Browse files
simonhollismeta-codesync[bot]
authored andcommitted
Wire ACL metadata into database open, create, finish, and backup
Summary: # What has changed Integrates ACL metadata loading and ACL-aware query slicing into the database lifecycle (Open, Create, Finish, Backup): - **Open.hs** (236 net new lines): Loads firstACLID and ACL group mapping from DB metadata at open time, populating new OpenDB fields (odbACLMode, odbACLMapping, odbFirstACLID). Adds ACL-aware query slicing via buildLayerACLSlice and withStackedACLSlices, which construct ownership slices that include ownership units + matched ACL groups. The readDatabaseWithBoundaries and withOpenDBLookup APIs gain an [Text] ACL group names parameter. - **Create.hs** (28 net new lines): Inherits base DB's firstACLID and ACL group mapping for incremental/stacked databases. Propagates the inherited GUID through the Storage.Create constructor (now 4-arg). Logs ACL mode at create time. - **Finish.hs** (7 new lines): Reads and logs ACL mode from database properties at finalization time for debugging. - **Backup.hs** (1 new line): Adds import for ACL-related types. # Why the change? For ACL filtering to work at query time, the server must load ACL metadata (firstACLID boundary, group→UsetId mapping) when opening a database, and construct appropriate ownership slices that restrict query results to facts matching the caller's ACL groups. For stacked/incremental DBs, ACL metadata must be inherited from the base DB to maintain consistent ACL ID assignments across the stack. The Create and Finish changes ensure ACL state is properly logged and propagated through the DB lifecycle. # Most important changes - `buildLayerACLSlice` in Open.hs (lines 270-380): Core ACL slice construction. Resolves ALL ACL group UnitIds via Storage.getUnitId, checks if they fall within the ownership range, and builds a combined slice of ownership units + matched ACL groups. Handles stacked namespace mismatch by falling back to firstACLID boundary. Risk: resolves every ACL group name on every query — could be expensive with many groups. - `withOpenDBLookup` refactored in Open.hs (lines 160-230): Now handles 4 cases: (no slicing, base-only, top-only, both). Each case constructs a different sliced/stacked lookup chain. Risk: complex nesting of withCanLookup callbacks — hard to verify correctness. - `withStackedACLSlices` in Open.hs (lines 390-410): Recursive walk of DB stack building ACL slices per layer. Risk: unbounded recursion on deep stacks. - OpenDB field population in Open.hs (lines 723-745): Loads firstACLID and ACL group mapping after OpenDB creation, then replaces the OpenDB with updated fields. Uses parseACLGroupMappingJson from Data.hs. - `readDatabaseWithBoundaries` signature change (lines 254-265): New [Text] parameter is a breaking API change — all callers must be updated. - `setupSchema` pattern match change (line 491): Create constructor now takes 4 arguments (added GUID parameter from commit 8). Simplify withStackedACLLookup and move ACL slice construction to exclude-mode semantics in Open.hs (folds in withStackedACLLookup simplification, formerly D108629297). Reviewed By: CatherineGasnier Differential Revision: D97111663 fbshipit-source-id: 6da8ebd37c0d89965df6c2a635cd15501f131333
1 parent b297946 commit 48b8864

6 files changed

Lines changed: 237 additions & 33 deletions

File tree

glean/db/Glean/Database/Create.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ kickOffDatabase env@Env{..} kickOff@Thrift.KickOff{..}
161161
})
162162
OpenDB{..} <- unmask $ Async.wait opener
163163
addSchemaIdProperty envCatalog kickOff_repo (schemaId odbSchema)
164+
-- Log ACL mode for this create operation
165+
let aclMode = getACLMode allProps
166+
logInfo $ inRepo kickOff_repo $ showACLMode aclMode
164167
return $ Thrift.KickOffResponse
165168
{ kickOffResponse_alreadyExists = False
166169
, kickOffResponse_auth_status = Nothing

glean/db/Glean/Database/Data.hs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ module Glean.Database.Data
1515
, retrieveSlices
1616
, storeACLGroupMapping
1717
, retrieveACLGroupMapping
18-
, parseACLGroupMappingJson
1918
, storeACLConfigHash
2019
, retrieveACLConfigHash
2120
, storeFirstACLID
@@ -114,9 +113,22 @@ retrieveSlices repo db = do
114113
storeACLGroupMapping :: DatabaseOps db => db -> ByteString -> IO ()
115114
storeACLGroupMapping db = Storage.store db aCL_GROUP_MAPPING_KEY
116115

117-
-- | Retrieve the ACL group name-to-UsetId mapping JSON blob.
118-
retrieveACLGroupMapping :: DatabaseOps db => db -> IO (Maybe ByteString)
119-
retrieveACLGroupMapping db = Storage.retrieve db aCL_GROUP_MAPPING_KEY
116+
-- | Retrieve and parse the ACL group name-to-UsetId mapping.
117+
-- Returns 'Nothing' when no mapping is stored. If a value is stored but fails
118+
-- to parse as valid JSON, logs a warning (so corrupted data is not silently
119+
-- mistaken for an empty mapping) and returns 'Nothing'.
120+
retrieveACLGroupMapping
121+
:: DatabaseOps db => db -> IO (Maybe (HashMap.HashMap Text Word32))
122+
retrieveACLGroupMapping db = do
123+
mBytes <- Storage.retrieve db aCL_GROUP_MAPPING_KEY
124+
case mBytes of
125+
Nothing -> return Nothing
126+
Just bytes -> case parseACLGroupMappingJson bytes of
127+
Just mapping -> return (Just mapping)
128+
Nothing -> do
129+
logWarning
130+
"corrupted acl_group_mapping, ignoring stored ACL group mapping"
131+
return Nothing
120132

121133
-- | Store the ACL configuration hash.
122134
storeACLConfigHash :: DatabaseOps db => db -> Text -> IO ()

glean/db/Glean/Database/Open.hs

Lines changed: 208 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,22 @@ import Control.Exception hiding(try)
2727
import Control.Monad.Catch (try)
2828
import Control.Monad.Extra
2929
import Data.ByteString (ByteString)
30+
import Data.Coerce (coerce)
3031
import Data.HashMap.Strict (HashMap)
3132
import qualified Data.HashMap.Strict as HashMap
3233
import qualified Data.Map as Map
3334
import Data.IORef
35+
import Data.List (sort)
3436
import Data.Maybe
3537
import Data.Set (Set)
38+
import Data.Text (Text)
3639
import qualified Data.Text as Text
40+
import qualified Data.Text.Encoding as TextE
3741
import Data.Word
3842
import qualified Data.Set as Set
3943
import GHC.Stack (HasCallStack)
4044

41-
import Util.Control.Exception (tryAll)
45+
import Util.Control.Exception
4246
import qualified Util.Control.Exception.CallStack as CallStack
4347
import Util.Log
4448
import Util.Logger
@@ -47,10 +51,14 @@ import Util.STM
4751
import qualified Glean.Database.Catalog as Catalog
4852
import Glean.Database.Catalog.Filter (Locality(..))
4953
import Glean.Database.Data
54+
( storeSchema, retrieveSchema
55+
, retrieveUnits, retrieveSlices, storeUnits, storeSlices
56+
, retrieveFirstACLID
57+
, retrieveACLGroupMapping )
5058
import Glean.Database.Exception
5159
import Glean.Database.Repo
5260
import Glean.Database.Storage as Storage
53-
import Glean.Database.Meta (Meta(..), ACLMode(..))
61+
import Glean.Database.Meta (Meta(..), getACLMode, ACLMode(..), showACLMode)
5462
import Glean.Database.Schema
5563
import Glean.Database.Schema.Types
5664
import Glean.Database.Types
@@ -62,13 +70,13 @@ import qualified Glean.RTS.Foreign.Lookup as Lookup
6270
import Glean.RTS.Foreign.Lookup (Lookup)
6371
import qualified Glean.RTS.Foreign.LookupCache as LookupCache
6472
import qualified Glean.RTS.Foreign.Ownership as Ownership
73+
import Glean.RTS.Foreign.Ownership (UnitId(..), UsetId(..))
6574
import qualified Glean.RTS.Foreign.Stacked as Stacked
6675
import Glean.RTS.Types (Pid(..))
6776
import qualified Glean.ServerConfig.Types as ServerConfig
68-
import Glean.Types (Repo)
6977
import qualified Glean.Internal.Types as Thrift
7078
import qualified Glean.Types as Thrift
71-
import Glean.Types (PredicateStats(..))
79+
import Glean.Types (Repo, PredicateStats(..))
7280
import Glean.Util.Mutex
7381
import qualified Glean.Util.Observed as Observed
7482
import Util.Time
@@ -149,36 +157,34 @@ repoParent Env{..} repo = do
149157
depParent :: Thrift.Dependencies -> Repo
150158
depParent deps = case deps of
151159
Thrift.Dependencies_pruned Thrift.Pruned{..} -> pruned_base
152-
Thrift.Dependencies_stacked Thrift.Stacked{..} -> Thrift.Repo stacked_name stacked_hash
160+
Thrift.Dependencies_stacked Thrift.Stacked{..} ->
161+
Thrift.Repo stacked_name stacked_hash
153162

154163
withOpenDBLookup
155164
:: Env
156165
-> Repo
157166
-> OpenDB
167+
-> [Text] -- ^ ACL group names for the user
158168
-> (Boundaries -> Lookup -> IO a)
159169
-> IO a
160-
withOpenDBLookup env repo OpenDB{ odbBaseSlices = baseSlices, .. } f =
170+
withOpenDBLookup env repo odb@OpenDB{ odbBaseSlices = baseSlices, .. }
171+
aclGroupNames f =
161172
Lookup.withCanLookup odbHandle $ \lookup -> do
162173
parent <- repoParent env repo
163174
case parent of
164175
Nothing -> do
165176
bounds <- flatBoundaries lookup
166-
f bounds lookup
177+
withFlatACLLookup env repo odb aclGroupNames lookup $ \sliced ->
178+
f bounds sliced
167179
Just baseRepo ->
168-
withOpenDatabase env baseRepo $ \OpenDB{..} -> do
169-
-- stacked (sliced base lookup)
170-
Lookup.withCanLookup odbHandle $ \baseLookup -> do
180+
withOpenDatabase env baseRepo $ \baseOdb@OpenDB{odbHandle = baseHandle} ->
181+
Lookup.withCanLookup baseHandle $ \baseLookup ->
171182
withOpenDBStack env baseRepo baseLookup $ \base -> do
172183
bounds <- stackedBoundaries base lookup
173-
let slices = catMaybes baseSlices
174-
if null slices
175-
then Lookup.withCanLookup (Stacked.stacked base lookup)
176-
(f bounds)
177-
else
178-
Lookup.withCanLookup
179-
(Ownership.slicedStack slices base) $ \sliced ->
180-
Lookup.withCanLookup (Stacked.stacked sliced lookup)
181-
(f bounds)
184+
let ownershipSlices = catMaybes baseSlices
185+
withStackedACLLookup env repo odb baseRepo baseOdb aclGroupNames
186+
ownershipSlices base lookup $ \stackedLookup ->
187+
f bounds stackedLookup
182188

183189
withOpenDBStack
184190
:: Env
@@ -211,19 +217,189 @@ readDatabase
211217
-> (OpenDB -> Lookup.Lookup -> IO a)
212218
-> IO a
213219
readDatabase env repo f =
214-
readDatabaseWithBoundaries env repo $ \odb _ lookup ->
220+
readDatabaseWithBoundaries env repo [] $ \odb _ lookup ->
215221
f odb lookup
216222

217223
readDatabaseWithBoundaries
218224
:: Env
219225
-> Repo
226+
-> [Text] -- ^ ACL group names for the user
220227
-> (OpenDB -> Boundaries -> Lookup -> IO a)
221228
-> IO a
222-
readDatabaseWithBoundaries env repo f =
229+
readDatabaseWithBoundaries env repo aclGroupNames f =
223230
withOpenDatabase env repo $ \odb ->
224-
withOpenDBLookup env repo odb $ \bounds lookup ->
231+
withOpenDBLookup env repo odb aclGroupNames $ \bounds lookup ->
225232
f odb bounds lookup
226233

234+
-- -----------------------------------------------------------------------------
235+
-- ACL Slice Construction
236+
237+
-- | Resolve ACL group names to their unit ids in this DB. Each group is stored
238+
-- as an "acl:<name>" ownership unit; names that don't resolve are dropped.
239+
resolveAclGroupUnits
240+
:: Storage.DatabaseOps db => db -> [Text] -> IO [Word32]
241+
resolveAclGroupUnits handle names =
242+
fmap catMaybes $ forM names $ \name -> do
243+
let unitName = TextE.encodeUtf8 ("acl:" <> name)
244+
mId <- Storage.getUnitId handle unitName
245+
return $ fmap (\(UnitId uid) -> uid) mId
246+
247+
-- | Read a DB's ACL metadata at open time: the firstACLID boundary and the
248+
-- group name -> UsetId mapping. Both are DB-invariant; empty/Nothing for
249+
-- DBs without ACLs.
250+
loadAclMetadata
251+
:: Storage.DatabaseOps db
252+
=> db
253+
-> IO (Maybe UsetId, Map.Map Text UsetId)
254+
loadAclMetadata handle = do
255+
mFirstACLID <- retrieveFirstACLID handle
256+
mMapping <- retrieveACLGroupMapping handle
257+
let aclMapping = case mMapping of
258+
Nothing -> Map.empty
259+
Just parsed -> Map.fromList
260+
[ (name, UsetId uid)
261+
| (name, uid) <- HashMap.toList parsed ]
262+
return (mFirstACLID, aclMapping)
263+
264+
-- | Build an ACL-aware ownership slice for a single DB layer, and pass it to
265+
-- a continuation. Passes Nothing if ACL filtering is not needed for this layer.
266+
--
267+
-- The slice makes file units (non-ACL units) visible by default, and ACL
268+
-- groups invisible unless they match the query's ACL group names.
269+
--
270+
-- We build the slice in EXCLUDE mode: the units we pass are the ACL-group
271+
-- units the caller is NOT a member of. Everything else (file
272+
-- units, and the caller's own groups) is visible by default, so a fact is
273+
-- hidden exactly when its ACL CNF requires a group the caller lacks.
274+
--
275+
-- The full set of ACL-group units is the contiguous range
276+
-- [firstACLID, firstACLID + #groups) (see registerOwnershipUnits); we
277+
-- subtract the caller's own groups (resolved via Storage.getUnitId) and
278+
-- exclude the rest.
279+
buildLayerACLSlice
280+
:: Env
281+
-> Repo
282+
-> OpenDB
283+
-> [Text] -- ^ ACL group names from request
284+
-> (Maybe Ownership.Slice -> IO a)
285+
-> IO a
286+
buildLayerACLSlice _env _repo odb aclGroupNames f = do
287+
case odbFirstACLID odb of
288+
Nothing -> do
289+
vlog 1 "[ACL-Slice] No firstACLID in DB, skipping ACL slice"
290+
f Nothing
291+
Just (UsetId firstACL) -> do
292+
let dbAclMode = odbACLMode odb
293+
let aclEnabled = dbAclMode /= ACLDisabled
294+
if not aclEnabled
295+
then do
296+
vlog 1 "[ACL-Slice] ACL mode disabled, skipping ACL slice"
297+
f Nothing
298+
else do
299+
-- Get ownership for this layer
300+
maybeOwnership <- readTVarIO (odbOwnership odb)
301+
case maybeOwnership of
302+
Nothing -> do
303+
vlog 1 "[ACL-Slice] No ownership data, skipping ACL slice"
304+
f Nothing
305+
Just ownership -> do
306+
-- All ACL-group units occupy the contiguous range
307+
-- [firstACLID, firstACLID + #groups) above every regular
308+
-- file unit (see registerOwnershipUnits at completion).
309+
let numGroups = Map.size (odbACLMapping odb)
310+
allGroups
311+
| numGroups == 0 = []
312+
| otherwise =
313+
[firstACL .. firstACL + fromIntegral numGroups - 1]
314+
userGroups <- resolveAclGroupUnits (odbHandle odb) aclGroupNames
315+
vlog 1 $ "[ACL-Slice] (exclude) allGroups="
316+
++ show (length allGroups)
317+
++ ", userGroups=" ++ show (length userGroups)
318+
-- Exclude (in EXCLUDE mode) the ACL-group units the caller is
319+
-- NOT a member of; everything else stays visible by default.
320+
let userSet = Set.fromList userGroups
321+
excludeUnits = coerce
322+
(sort (filter (`Set.notMember` userSet) allGroups))
323+
:: [UnitId]
324+
aclSlice <- Ownership.slice ownership [] excludeUnits True
325+
vlog 1 "[ACL-Slice] Slice created successfully"
326+
f (Just aclSlice)
327+
328+
-- | Wrap a lookup with the given ownership/ACL slices, or pass it through
329+
-- unchanged when there are none. An empty list must NOT be handed to
330+
-- slicedStack: an empty Slices treats every UsetId as not-covered and hides
331+
-- all facts.
332+
withMaybeSliced
333+
:: [Ownership.Slice] -> Lookup -> (Lookup -> IO a) -> IO a
334+
withMaybeSliced [] lk k = k lk
335+
withMaybeSliced slices lk k =
336+
Lookup.withCanLookup (Ownership.slicedStack slices lk) k
337+
338+
-- | Apply the ACL slice (if any) for a flat (non-stacked) database and
339+
-- run the continuation with the resulting (possibly sliced) lookup.
340+
withFlatACLLookup
341+
:: Env
342+
-> Repo
343+
-> OpenDB
344+
-> [Text] -- ^ ACL group names for the user
345+
-> Lookup -- ^ the layer's lookup
346+
-> (Lookup -> IO a)
347+
-> IO a
348+
withFlatACLLookup env repo odb aclGroupNames lookup f =
349+
buildLayerACLSlice env repo odb aclGroupNames $ \mAclSlice ->
350+
withMaybeSliced (maybeToList mAclSlice) lookup f
351+
352+
-- | Build ACL slices for all layers in a stacked DB (base layers only).
353+
-- Walks the DB stack recursively and builds an ACL slice for each layer
354+
-- that has ACL data (firstACLID). Returns the list of ACL slices.
355+
withStackedACLSlices
356+
:: Env
357+
-> Repo -- ^ Base repo (immediate parent)
358+
-> OpenDB -- ^ Base OpenDB
359+
-> [Text] -- ^ ACL group names
360+
-> ([Ownership.Slice] -> IO a)
361+
-> IO a
362+
withStackedACLSlices env baseRepo baseOdb aclGroupNames f = do
363+
-- Build ACL slice for this base layer
364+
buildLayerACLSlice env baseRepo baseOdb aclGroupNames $ \mSlice -> do
365+
-- Recurse to deeper layers
366+
parent <- repoParent env baseRepo
367+
case parent of
368+
Nothing -> f (maybeToList mSlice)
369+
Just parentRepo ->
370+
withOpenDatabase env parentRepo $ \parentOdb ->
371+
withStackedACLSlices env parentRepo parentOdb aclGroupNames
372+
$ \parentSlices ->
373+
f (maybeToList mSlice ++ parentSlices)
374+
375+
-- | Apply ownership + ACL slices for a stacked database and run the
376+
-- continuation with the combined stacked lookup. This wires together the
377+
-- base ACL slices (walked over the base stack), the top-layer ACL slice,
378+
-- and the pre-existing ownership slices into a single sliced/stacked lookup.
379+
withStackedACLLookup
380+
:: Env
381+
-> Repo -- ^ Top (stacked) repo
382+
-> OpenDB -- ^ Top OpenDB
383+
-> Repo -- ^ Base repo (immediate parent)
384+
-> OpenDB -- ^ Base OpenDB
385+
-> [Text] -- ^ ACL group names for the user
386+
-> [Ownership.Slice] -- ^ Ownership slices for the base stack
387+
-> Lookup -- ^ Base stack lookup
388+
-> Lookup -- ^ Top layer lookup
389+
-> (Lookup -> IO a)
390+
-> IO a
391+
withStackedACLLookup env repo odb baseRepo baseOdb aclGroupNames
392+
ownershipSlices base lookup f =
393+
-- Build ACL slices for base layers (walk the base stack)
394+
withStackedACLSlices env baseRepo baseOdb aclGroupNames $ \baseACLSlices ->
395+
-- Build ACL slice for the top layer
396+
buildLayerACLSlice env repo odb aclGroupNames $ \mTopACLSlice ->
397+
-- Slice each side only if it has slices (empty would hide everything),
398+
-- then stack the (possibly sliced) base under the top layer.
399+
withMaybeSliced (ownershipSlices ++ baseACLSlices) base $ \slicedBase ->
400+
withMaybeSliced (maybeToList mTopACLSlice) lookup $ \slicedTop ->
401+
Lookup.withCanLookup (Stacked.stacked slicedBase slicedTop) f
402+
227403
newDB :: Repo -> STM DB
228404
newDB repo = DB repo
229405
<$> newTVar Closed
@@ -527,6 +703,13 @@ asyncOpenDB env@Env{..} storage db@DB{..} version mode deps
527703
Just slices -> return $ map Just slices
528704
idle <- newTVarIO =<< getTimePoint
529705
ownership <- newTVarIO =<< Storage.getOwnership handle
706+
-- Get database properties to check ACL mode
707+
meta <- atomically $ Catalog.readMeta envCatalog dbRepo
708+
let aclMode = getACLMode (metaProperties meta)
709+
-- Log ACL metadata on database open
710+
logInfo $ inRepo dbRepo $
711+
"[Open.hs] Database opening, ACL mode: " ++ showACLMode aclMode
712+
(mFirstACLID, aclMapping) <- loadAclMetadata handle
530713
on_success
531714
return OpenDB
532715
{ odbHandle = Some handle
@@ -535,9 +718,9 @@ asyncOpenDB env@Env{..} storage db@DB{..} version mode deps
535718
, odbIdleSince = idle
536719
, odbBaseSlices = maybeSlices
537720
, odbOwnership = ownership
538-
, odbACLMode = ACLDisabled
539-
, odbACLMapping = Map.empty
540-
, odbFirstACLID = Nothing
721+
, odbACLMode = aclMode
722+
, odbACLMapping = aclMapping
723+
, odbFirstACLID = mFirstACLID
541724
}
542725
atomically $ writeTVar dbState $ Open odb
543726
enqueueBatchDescriptorsFromDatabase env db odb
@@ -562,7 +745,6 @@ enqueueBatchDescriptorsFromDatabase env DB{..} OpenDB{..} =
562745
odbSchema odbHandle descriptor True
563746
Nothing -> return ()
564747

565-
566748
-- | Fetch the glean.schema_id property of a DB, if it has one. This
567749
-- property is used to resolve queries.
568750
getDbSchemaVersion

glean/db/Glean/Database/Types.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ import Glean.Logger.Database (GleanDatabaseLogger)
5858
import Glean.RTS.Foreign.FactSet (FactSet)
5959
import Glean.RTS.Foreign.LookupCache (LookupCache)
6060
import qualified Glean.RTS.Foreign.LookupCache as LookupCache
61-
import Glean.RTS.Foreign.Ownership (Ownership, Slice, DefineOwnership, UsetId)
61+
import Glean.RTS.Foreign.Ownership
62+
(Ownership, Slice, DefineOwnership, UsetId)
6263
import Glean.RTS.Foreign.Subst (Subst)
6364
import Glean.RTS.Types (Fid(..))
6465
import qualified Glean.ServerConfig.Types as ServerConfig

glean/db/Glean/Query/Derive.hs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,10 @@ runDerivation
313313
-> Thrift.DerivePredicateQuery
314314
-> IO ()
315315
runDerivation env repo ref pred Thrift.DerivePredicateQuery{..} = do
316-
readDatabaseWithBoundaries env repo $ \odb bounds lookup ->
316+
-- TODO: thread the caller's ACL group names through here instead of [].
317+
-- With [] and ACL mode enabled, exclude-mode slicing hides all
318+
-- ACL-restricted facts from the derivation path.
319+
readDatabaseWithBoundaries env repo [] $ \odb bounds lookup ->
317320
case derivePredicateQuery_parallel of
318321
Nothing -> deriveQuery odb bounds lookup (query (allFacts ref))
319322
Just par -> parallelDerivation odb bounds lookup par

0 commit comments

Comments
 (0)