Skip to main content

Entity Resolution with .on()

All authorization checks in cedar4s use the .on(id) pattern, which loads entities and resolves parent relationships automatically.

Examples below assume import myapp.cedar.MyApp.* is in scope.

Overview​

The .on(id) pattern takes a typed entity ID:

// Check if user can read document - parents loaded automatically
Document.Read.on(DocumentId("doc-1")).require

This loads the document from your database, extracts its parent relationships (e.g., folder), and performs the authorization check with the full entity hierarchy.

Setup​

Authorization checks require CedarSession and FlatMap in scope:

given CedarSession[Future] = runtime.session(currentUser)
given FlatMap[Future] = FlatMap.futureInstance

// Authorization checks work
Document.Read.on(DocumentId("doc-1")).require

How It Works​

The flow:

  1. .on(id) creates an AuthCheck with the typed entity ID
  2. When executed, it calls EntityStore.loadEntityWithParents(entityType, id)
  3. Your EntityFetcher.fetch(id) is called with the typed ID
  4. The entity's parents are extracted via toCedarEntity().parents
  5. A ResourceRef is built with entity type, ID, and parent IDs
  6. The authorization check executes with the resolved resource

Entity Parent Extraction​

For authorization checks to work, your Cedar entities must define their parent relationships. cedar4s generates this from your schema:

namespace DocShare {
entity Folder {}
entity Document in [Folder] {}
}

Generated entity includes parent reference with typed IDs:

case class Document(
id: DocumentId,
folderId: FolderId, // Parent reference
// ... other attributes
) extends CedarEntity {
def parents: List[(String, String)] =
List(("DocShare::Folder", folderId.value))
}

Usage Patterns​

Basic Check​

// Load document, extract folder parent, check authorization
Document.Read.on(DocumentId("doc-1")).require

With Context​

Document.Read.on(DocumentId("doc-1"))
.withContext(ReadContext(requestTime = Instant.now()))
.require

Conditional Check​

Document.Read.on(DocumentId("doc-1"))
.when(featureEnabled)
.require

Composition​

Checks can be composed, with each entity resolved independently:

// Both documents loaded separately
val check =
Document.Read.on(DocumentId("doc-1")) &
Document.Read.on(DocumentId("doc-2"))

check.require

Entity Loading​

Each authorization check loads the entity and its parents from your data store. This ensures Cedar always has the current state of the entity hierarchy.

OperationWhat Happens
Document.Read.on(id)Load document, extract parents
Folder.View.on(id)Load folder (no parents for root entities)

Caching​

Enable EntityStore caching to avoid repeated database hits for the same entity:

import cedar4s.caffeine.{CaffeineEntityCache, CaffeineCacheConfig}

val cache = CaffeineEntityCache[Future](CaffeineCacheConfig.default)

val store = EntityStore.builder[Future]()
.register[Entities.Document, DocumentId](new DocumentFetcher(db))
.register[Entities.Folder, FolderId](new FolderFetcher(db))
.withCache(cache)
.build()

With caching, frequently accessed entities are loaded once and reused across authorization checks.