Files
Onsol-GO/.agents/skills/firebase-data-connect/reference/security.md
T
2026-04-23 23:58:59 -05:00

7.0 KiB

Security Reference

Contents


@auth Directive

Every deployable query/mutation must have @auth. Without it, operations default to NO_ACCESS.

query PublicData @auth(level: PUBLIC) { ... }
query UserData @auth(level: USER) { ... }
query AdminOnly @auth(expr: "auth.token.admin == true") { ... }
Argument Description
level Preset access level
expr CEL expression (alternative to level)
insecureReason Suppress deploy warning for PUBLIC/unfiltered USER

Access Levels

Level Who Can Access CEL Equivalent
PUBLIC Anyone, authenticated or not true
USER_ANON Any authenticated user (including anonymous) auth.uid != nil
USER Authenticated users (excludes anonymous) auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'
USER_EMAIL_VERIFIED Users with verified email auth.uid != nil && auth.token.email_verified
NO_ACCESS Admin SDK only false

Important: Levels like USER are starting points. Always add filters or expressions to verify the user can access specific data.


CEL Expressions

Available Bindings

Binding Description
auth.uid Current user's Firebase UID
auth.token Auth token claims (see below)
vars Operation variables (e.g., vars.movieId)
request.time Server timestamp
request.operationName "query" or "mutation"

auth.token Fields

Field Description
email User's email address
email_verified Boolean: email verified
phone_number User's phone
name Display name
sub Firebase UID (same as auth.uid)
firebase.sign_in_provider password, google.com, anonymous, etc.
<custom_claim> Custom claims set via Admin SDK

Expression Examples

# Check custom claim
@auth(expr: "auth.token.role == 'admin'")

# Check verified email domain
@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')")

# Check multiple conditions
@auth(expr: "auth.uid != nil && (auth.token.role == 'editor' || auth.token.role == 'admin')")

# Check variable
@auth(expr: "has(vars.status) && vars.status in ['draft', 'published']")

Using eq_expr in Filters

Compare database fields with auth values:

query MyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) {
    id title
  }
}

mutation UpdateMyPost($id: UUID!, $title: String!) @auth(level: USER) {
  post_update(
    first: { where: {
      id: { eq: $id },
      authorUid: { eq_expr: "auth.uid" }
    }},
    data: { title: $title }
  )
}

@check and @redact

Use @check to validate data and @redact to hide results from client:

@check

Validates a field value; aborts if check fails.

@check(expr: "this != null", message: "Not found")
@check(expr: "this == 'editor'", message: "Must be editor")
@check(expr: "this.exists(p, p.role == 'admin')", message: "No admin found")
Argument Description
expr CEL expression; this = current field value
message Error message if check fails
optional If true, pass when field not present

@redact

Hides field from response (still evaluated for @check):

query @redact { ... }  # Query result hidden but @check still runs

Authorization Data Lookup

Check database permissions before allowing mutation:

mutation UpdateMovie($id: UUID!, $title: String!) 
  @auth(level: USER) 
  @transaction {
  # Step 1: Check user has permission
  query @redact {
    moviePermission(
      key: { movieId: $id, userId_expr: "auth.uid" }
    ) @check(expr: "this != null", message: "No access to movie") {
      role @check(expr: "this == 'editor'", message: "Must be editor")
    }
  }
  # Step 2: Update if authorized
  movie_update(id: $id, data: { title: $title })
}

Validate Key Exists

mutation MustDeleteMovie($id: UUID!) @auth(level: USER) @transaction {
  movie_delete(id: $id) 
    @check(expr: "this != null", message: "Movie not found")
}

Authorization Patterns

User-Owned Resources

# Create with owner
mutation CreatePost($content: String!) @auth(level: USER) {
  post_insert(data: {
    authorUid_expr: "auth.uid",
    content: $content
  })
}

# Read own data only
query MyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) {
    id content
  }
}

# Update own data only
mutation UpdatePost($id: UUID!, $content: String!) @auth(level: USER) {
  post_update(
    first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}},
    data: { content: $content }
  )
}

# Delete own data only
mutation DeletePost($id: UUID!) @auth(level: USER) {
  post_delete(
    first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}}
  )
}

Role-Based Access

# Admin-only query
query AllUsers @auth(expr: "auth.token.admin == true") {
  users { id email name }
}

# Role from database
mutation AdminAction($id: UUID!) @auth(level: USER) @transaction {
  query @redact {
    user(key: { uid_expr: "auth.uid" }) {
      role @check(expr: "this == 'admin'", message: "Admin required")
    }
  }
  # ... admin action
}

Public Data with Filters

query PublicPosts @auth(level: PUBLIC) {
  posts(where: {
    visibility: { eq: "public" },
    publishedAt: { lt_expr: "request.time" }
  }) {
    id title content
  }
}

Tiered Access (Pro Content)

query ProContent @auth(expr: "auth.token.plan == 'pro'") {
  posts(where: { visibility: { in: ["public", "pro"] }}) {
    id title content
  }
}

Anti-Patterns

Don't Pass User ID as Variable

# BAD - any user can pass any userId
query GetUserPosts($userId: String!) @auth(level: USER) {
  posts(where: { authorUid: { eq: $userId }}) { ... }
}

# GOOD - use auth.uid
query GetMyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) { ... }
}

Don't Use USER Without Filters

# BAD - any authenticated user sees all documents
query AllDocs @auth(level: USER) {
  documents { id title content }
}

# GOOD - filter to user's documents
query MyDocs @auth(level: USER) {
  documents(where: { ownerId: { eq_expr: "auth.uid" }}) { ... }
}

Don't Trust Unverified Email

# BAD - email not verified
@auth(expr: "auth.token.email.endsWith('@company.com')")

# GOOD - verify email first
@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')")

Don't Use PUBLIC/USER for Prototyping

During development, set operations to NO_ACCESS until you implement proper authorization. Use emulator and VS Code extension for testing.