Batch Authorization
Check multiple resources efficiently with batch operations.
Examples below assume import myapp.cedar.MyApp.* is in scope.
Overview
CedarSession[F] provides batch methods for multi-resource authorization:
trait CedarSession[F[_]] {
// Execute multiple requests, returning results for each
def batchRun(requests: Seq[AuthCheck[?, ?, ?]]): F[Seq[Either[CedarAuthError, Unit]]]
// Check multiple requests, returning boolean for each
def batchIsAllowed(requests: Seq[AuthCheck[?, ?, ?]]): F[Seq[Boolean]]
// Filter items to only those the principal can access
def filterAllowed[A](items: Seq[A])(toRequest: A => AuthCheck[?, ?, ?]): F[Seq[A]]
}
filterAllowed
Filter a collection to only permitted items:
import myapp.cedar.MyApp.*
// Setup: Create session for current user
given session: CedarSession[Future] = runtime.session(currentUser)
val documents: Seq[Document] = loadDocuments()
val allowed: Future[Seq[Document]] = session.filterAllowed(documents) { doc =>
Document.Read.on(DocumentId(doc.id))
}
// Returns only documents the user can read
batchIsAllowed
Get boolean results for each item:
given session: CedarSession[Future] = runtime.session(currentUser)
val documents: Seq[Document] = loadDocuments()
val checks = documents.map(doc => Document.Read.on(DocumentId(doc.id)))
val results: Future[Seq[Boolean]] = session.batchIsAllowed(checks)
// Seq(true, false, true, ...)
Useful when you need to show all items but indicate which are accessible.
batchRun
Get detailed results including error information:
given session: CedarSession[Future] = runtime.session(currentUser)
val checks = documents.map(doc => Document.Read.on(DocumentId(doc.id)))
val results: Future[Seq[Either[CedarAuthError, Unit]]] = session.batchRun(checks)
results.map { outcomes =>
outcomes.zipWithIndex.foreach { case (result, i) =>
result match {
case Right(()) => println(s"Document ${i}: allowed")
case Left(err) => println(s"Document ${i}: denied - ${err.message}")
}
}
}
Batch Flow
Batch operations:
- Collect all resource references from the requests
- Load all entities in a single batch (using
fetchBatchif implemented) - Evaluate all authorization requests against the loaded entities
- Return results
Performance Impact
Without batch operations (N individual checks):
Document 1: load entities (5ms) + evaluate (1ms) = 6ms
Document 2: load entities (5ms) + evaluate (1ms) = 6ms
...
Document 100: load entities (5ms) + evaluate (1ms) = 6ms
Total: ~600ms
With batch operations:
Load all entities once (5ms) + evaluate all (10ms) = 15ms
Total: ~15ms (40x faster)
Implementing fetchBatch
For batch operations to be efficient, implement fetchBatch in your EntityFetcher:
class DocumentFetcher(db: Database)(using ec: ExecutionContext)
extends EntityFetcher[Future, Entities.Document, DocumentId] {
def fetch(id: DocumentId): Future[Option[Entities.Document]] =
db.findDocument(id.value).map(_.map(toCedar))
// Single SQL query for all IDs
override def fetchBatch(ids: Set[DocumentId])(using Applicative[Future]): Future[Map[DocumentId, Entities.Document]] =
db.run(documents.filter(_.id.inSet(ids.map(_.value))).result).map { docs =>
docs.map(d => DocumentId(d.id) -> toCedar(d)).toMap
}
}
See Entity Fetchers for details.
Practical Examples
List Page with Permissions
def listDocuments(folderId: FolderId)(using session: CedarSession[Future]): Future[Seq[DocumentWithPermissions]] = {
for {
docs <- documentService.listByFolder(folderId.value)
// Check read permission for display
readable <- session.batchIsAllowed(docs.map(d => Document.Read.on(DocumentId(d.id))))
// Check edit permission for UI
editable <- session.batchIsAllowed(docs.map(d => Document.Edit.on(DocumentId(d.id))))
} yield {
docs.zip(readable).zip(editable).map { case ((doc, canRead), canEdit) =>
DocumentWithPermissions(doc, canRead = canRead, canEdit = canEdit)
}
}
}
Bulk Operations
def deleteDocuments(docIds: Seq[String])(using session: CedarSession[Future]): Future[BulkDeleteResult] = {
for {
docs <- documentService.getMany(docIds)
// Filter to only deletable documents
deletable <- session.filterAllowed(docs)(d => Document.Delete.on(DocumentId(d.id)))
// Delete allowed documents
_ <- documentService.deleteMany(deletable.map(_.id))
} yield {
BulkDeleteResult(
deleted = deletable.size,
denied = docIds.size - deletable.size
)
}
}