PRE-RELEASE INFORMATION: SUBJECT TO CHANGE

Authorization Requests: How to Ask Permission

This guide shows you how to integrate Housecarl authorization into your application. We'll cover the authorization request model, provide code examples in Rust, Go, and Python, and walk through common integration patterns.

The Authorization Question

Every authorization request asks: "Can this user do this action on this resource?"

In Housecarl, this becomes:

subject  → Who is asking?
action   → What do they want to do?
object   → What do they want to do it to?
context  → Additional attributes for matching

Anatomy of an Authorization Request

Here's a complete authorization request in JSON format:

{
  "context": {
    "subject": "alice",
    "action": "read",
    "object": "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/project-alpha/roadmap.pdf",
    "team": "engineering",
    "clearance_level": "confidential",
    "ip_address": "192.168.1.100"
  }
}

Let's break down each field.

subject (required)

The identity making the request. Usually a username or user ID.

Examples:

"subject": "alice"                    // Simple username
"subject": "user:alice@example.com"   // With namespace
"subject": "svc:billing-service"      // Service account

Best practice: Use a consistent identifier that appears in your policies. If your policies match on username, use username here.

action (required)

What the subject wants to do. This is application-defined - Housecarl doesn't enforce a vocabulary.

Examples:

"action": "read"
"action": "write"
"action": "delete"
"action": "approve-deployment"
"action": "create-invoice"

Best practice: Use a consistent set of actions across your application. Common patterns:

  • CRUD: create, read, update, delete
  • HTTP verbs: GET, POST, PUT, DELETE
  • Domain-specific: approve, reject, publish, archive

object (required)

The resource being accessed. Must include a Housecarl resource URI.

Format: a single string in Housecarl resource URI format.

"hc://domain/<domain-uuid>/path/to/resource"

Housecarl Resource URI format: hc://domain/<domain-uuid>/path/segments

Examples:

// Simple resource
"hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/project-alpha/spec.pdf"

// Hierarchical resource
"hc://domain/550e8400-e29b-41d4-a716-446655440000/api/v1/users/alice/profile"

// Environment-specific resource
"hc://domain/550e8400-e29b-41d4-a716-446655440000/environments/production/deploy"

Best practice: Design your resource URIs to enable hierarchical policies:

  • Use paths: hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/team/project/file.pdf
  • Enable wildcards: hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/engineering/* matches all engineering docs
  • Be consistent: Always use the same structure for the same resource type

context (optional)

Additional attributes that policies can match against. This is a flexible key-value map inside context.

Examples:

{
  "team": "engineering",
  "role": "senior-engineer",
  "department": "product",
  "clearance_level": "top-secret",
  "time_of_day": "business-hours",
  "ip_address": "192.168.1.100",
  "mfa_verified": "true"
}

Best practice: Include attributes that your policies need for matching:

  • User attributes: team, role, department, seniority
  • Request attributes: IP address, time, location
  • Resource attributes: classification, owner, sensitivity
  • Session attributes: MFA status, session age

Making Authorization Requests

Housecarl provides a gRPC API for authorization. Here's how to use it in different languages.

Authentication: protected gRPC requests require:

  • authorization: Bearer <token>
  • signed-by: <base64 hmac signature>
  • date-filed-in: <unix timestamp>

The signature is computed over the serialized protobuf request body with the signing_secret returned by Login (or stored by housectl config login in ~/.config/housecarl.toml). The Rust example below is the tested reference implementation.

Before You Run the Live Examples

The runnable examples on this page need a real domain UUID. Export one before you build or run anything:

export HOUSECARL_DOMAIN_ID=<real-domain-uuid>
export HOUSECARL_TOKEN=<bearer-token>
export HOUSECARL_SIGNING_SECRET=<base64-signing-secret>

If you leave the sample UUIDs from this page in a live request, Housecarl returns domain not found.

For the Go and Python gRPC examples, generate current stubs from housecarl_lib/proto/housecarl.proto first:

# Go
mkdir -p housecarlpb
protoc \
  -I <path-to-upside-down-research-code>/housecarl_lib/proto \
  --go_out=housecarlpb --go_opt=paths=source_relative \
  --go-grpc_out=housecarlpb --go-grpc_opt=paths=source_relative \
  <path-to-upside-down-research-code>/housecarl_lib/proto/housecarl.proto
go get google.golang.org/grpc google.golang.org/protobuf

# Python
python -m pip install grpcio grpcio-tools protobuf
python -m grpc_tools.protoc \
  -I <path-to-upside-down-research-code>/housecarl_lib/proto \
  --python_out=. \
  --grpc_python_out=. \
  <path-to-upside-down-research-code>/housecarl_lib/proto/housecarl.proto

Rust Example

In Cargo.toml:

base64 = "0.22"
hex = "0.4"
hmac = "0.12"
prost = "0.14"
sha2 = "0.10"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tonic = "0.14"
housecarl_lib = { path = "../housecarl_lib" }
use housecarl_lib::grpc::housecarl::{
    housecarl_service_client::HousecarlServiceClient, CheckAuthorizationRequest, RequestValue,
};
use base64::Engine;
use hmac::{Hmac, Mac};
use housecarl_lib::grpc::housecarl::request_value::Value as RequestValueKind;
use prost::Message;
use sha2::Sha256;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use tonic::Request;

type HmacSha256 = Hmac<Sha256>;

fn single(value: &str) -> RequestValue {
    RequestValue {
        value: Some(RequestValueKind::Single(value.to_string())),
    }
}

fn sign_request(
    body: &[u8],
    signing_secret_b64: &str,
    timestamp: u64,
) -> Result<String, Box<dyn std::error::Error>> {
    let secret = base64::engine::general_purpose::STANDARD.decode(signing_secret_b64)?;
    let time_bucket = timestamp / 300;
    let message = format!(
        "housecarl-request-v1:{}:{}:{}",
        body.len(),
        hex::encode(body),
        time_bucket
    );

    let mut mac = HmacSha256::new_from_slice(&secret)?;
    mac.update(message.as_bytes());
    Ok(base64::engine::general_purpose::STANDARD.encode(mac.finalize().into_bytes()))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = std::env::var("HOUSECARL_TOKEN")?;
    let signing_secret = std::env::var("HOUSECARL_SIGNING_SECRET")?;
    let domain_id = std::env::var("HOUSECARL_DOMAIN_ID")?;

    // Connect to housecarl server
    let mut client = HousecarlServiceClient::connect("http://localhost:50051").await?;

    // Build the authorization request
    let mut context = HashMap::new();
    context.insert("subject".to_string(), single("alice"));
    context.insert("action".to_string(), single("read"));
    context.insert(
        "object".to_string(),
        single(
            format!(
                "hc://domain/{}/documents/project-alpha/roadmap.pdf",
                domain_id
            )
            .as_str(),
        ),
    );
    context.insert("team".to_string(), single("engineering"));
    context.insert("role".to_string(), single("developer"));

    let authz_request = CheckAuthorizationRequest { context };
    let body = authz_request.encode_to_vec();
    let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
    let signature = sign_request(&body, &signing_secret, timestamp)?;

    // Make the request
    let mut request = Request::new(authz_request);
    request
        .metadata_mut()
        .insert("authorization", format!("Bearer {}", token).parse()?);
    request.metadata_mut().insert("signed-by", signature.parse()?);
    request
        .metadata_mut()
        .insert("date-filed-in", timestamp.to_string().parse()?);
    let response = client.check_authorization(request).await?;

    // Check the result
    let authz_response = response.into_inner();
    if authz_response.authorized {
        println!("Access granted!");
        // Proceed with the operation
        read_document("project-alpha/roadmap.pdf")?;
    } else {
        println!("Access denied");
        return Err("Authorization denied".into());
    }

    Ok(())
}

fn read_document(path: &str) -> Result<(), Box<dyn std::error::Error>> {
    // Your document reading logic
    println!("Reading document: {}", path);
    Ok(())
}

Key points:

  • Use the generated gRPC client (HousecarlServiceClient)
  • Build CheckAuthorizationRequest with a context map of RequestValues
  • Include the Bearer token plus signed-by and date-filed-in metadata
  • Check response.authorized before proceeding
  • Handle denied requests appropriately (log, return error, etc.)

Important: the Go and Python snippets below were validated with the same signing metadata pattern shown above. They assume you generated current local stubs from housecarl.proto before building.

Go Example

package main

import (
    "context"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "fmt"
    "log"
    "os"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/metadata"
    "google.golang.org/protobuf/proto"
    pb "your/module/housecarlpb"
)

func single(val string) *pb.RequestValue {
    return &pb.RequestValue{Value: &pb.RequestValue_Single{Single: val}}
}

func signRequest(body []byte, signingSecretB64 string, timestamp int64) (string, error) {
    secret, err := base64.StdEncoding.DecodeString(signingSecretB64)
    if err != nil {
        return "", fmt.Errorf("decode signing secret: %w", err)
    }

    timeBucket := timestamp / 300
    message := fmt.Sprintf(
        "housecarl-request-v1:%d:%s:%d",
        len(body),
        hex.EncodeToString(body),
        timeBucket,
    )

    mac := hmac.New(sha256.New, secret)
    if _, err := mac.Write([]byte(message)); err != nil {
        return "", fmt.Errorf("sign request: %w", err)
    }

    return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil
}

func main() {
    token := os.Getenv("HOUSECARL_TOKEN")
    signingSecret := os.Getenv("HOUSECARL_SIGNING_SECRET")
    domainID := os.Getenv("HOUSECARL_DOMAIN_ID")
    if token == "" {
        log.Fatal("HOUSECARL_TOKEN is required")
    }
    if signingSecret == "" {
        log.Fatal("HOUSECARL_SIGNING_SECRET is required")
    }
    if domainID == "" {
        log.Fatal("HOUSECARL_DOMAIN_ID is required")
    }

    // Connect to housecarl server
    conn, err := grpc.Dial(
        "localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        log.Fatalf("Failed to connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewHousecarlServiceClient(conn)

    // Build the authorization request
    req := &pb.CheckAuthorizationRequest{
        Context: map[string]*pb.RequestValue{
            "subject": single("alice"),
            "action":  single("read"),
            "object":  single(fmt.Sprintf("hc://domain/%s/documents/project-alpha/roadmap.pdf", domainID)),
            "team":    single("engineering"),
            "role":    single("developer"),
        },
    }

    body, err := proto.Marshal(req)
    if err != nil {
        log.Fatalf("Failed to marshal request: %v", err)
    }

    timestamp := time.Now().Unix()
    signature, err := signRequest(body, signingSecret, timestamp)
    if err != nil {
        log.Fatalf("Failed to sign request: %v", err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    ctx = metadata.NewOutgoingContext(
        ctx,
        metadata.Pairs(
            "authorization", "Bearer "+token,
            "signed-by", signature,
            "date-filed-in", fmt.Sprintf("%d", timestamp),
        ),
    )

    resp, err := client.CheckAuthorization(ctx, req)
    if err != nil {
        log.Fatalf("Authorization request failed: %v", err)
    }

    // Check the result
    if resp.Authorized {
        fmt.Println("Access granted!")
        // Proceed with the operation
        if err := readDocument("project-alpha/roadmap.pdf"); err != nil {
            log.Fatalf("Failed to read document: %v", err)
        }
    } else {
        log.Printf("Access denied")
        // Return error or redirect user
    }
}

func readDocument(path string) error {
    // Your document reading logic
    fmt.Printf("Reading document: %s\n", path)
    return nil
}

Key points:

  • Use context with timeout for gRPC calls
  • Check for both gRPC errors and authorization denial
  • Clean up resources with defer conn.Close()
  • Handle denials gracefully (log and return appropriate error)

Python Example

import os
import base64
import hashlib
import hmac
import time
import grpc
from housecarl_pb2 import CheckAuthorizationRequest, RequestValue
from housecarl_pb2_grpc import HousecarlServiceStub

def single(value: str) -> RequestValue:
    return RequestValue(single=value)

def check_authorization(user, action, resource, context=None):
    """
    Check if a user is authorized to perform an action on a resource.

    Args:
        user: Username or user ID
        action: Action to perform (e.g., 'read', 'write')
        resource: Resource URI (e.g., 'hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/...')
        context: Optional dict of additional attributes

    Returns:
        True if authorized, False otherwise
    """
    token = os.environ["HOUSECARL_TOKEN"]
    signing_secret = os.environ["HOUSECARL_SIGNING_SECRET"]

    # Connect to housecarl server
    channel = grpc.insecure_channel("localhost:50051")
    client = HousecarlServiceStub(channel)

    # Build the request
    ctx = {
        "subject": single(user),
        "action": single(action),
        "object": single(resource),
    }
    for key, value in (context or {}).items():
        ctx[key] = single(value)

    request = CheckAuthorizationRequest(context=ctx)
    body = request.SerializeToString()
    timestamp = int(time.time())
    time_bucket = timestamp // 300
    message = f"housecarl-request-v1:{len(body)}:{body.hex()}:{time_bucket}".encode("utf-8")
    signature = base64.b64encode(
        hmac.new(base64.b64decode(signing_secret), message, hashlib.sha256).digest()
    ).decode("utf-8")

    try:
        response = client.CheckAuthorization(
            request,
            metadata=[
                ("authorization", f"Bearer {token}"),
                ("signed-by", signature),
                ("date-filed-in", str(timestamp)),
            ],
        )

        if response.authorized:
            print(f"Access granted for {user} to {action} {resource}")
            return True
        else:
            print("Access denied")
            return False

    except grpc.RpcError as e:
        print(f"Authorization request failed: {e.code()}: {e.details()}")
        # Fail closed: deny on error
        return False
    finally:
        channel.close()

# Example usage
def main():
    domain_id = os.environ["HOUSECARL_DOMAIN_ID"]

    # Simple authorization check
    if check_authorization(
        user="alice",
        action="read",
        resource=f"hc://domain/{domain_id}/documents/project-alpha/roadmap.pdf"
    ):
        read_document("project-alpha/roadmap.pdf")
    else:
        print("Access denied. Cannot read document.")

    # Authorization with context attributes
    if check_authorization(
        user="bob",
        action="deploy",
        resource=f"hc://domain/{domain_id}/environments/production",
        context={
            "team": "platform",
            "mfa_verified": "true",
            "time_of_day": "business-hours"
        }
    ):
        deploy_to_production()
    else:
        print("Deployment denied. Check permissions and MFA status.")

def read_document(path):
    """Read a document - placeholder"""
    print(f"Reading document: {path}")

def deploy_to_production():
    """Deploy to production - placeholder"""
    print("Deploying to production...")

if __name__ == "__main__":
    main()

Key points:

  • Wrap authorization in a reusable function
  • Handle gRPC errors gracefully
  • Fail closed: deny on errors
  • Close channel after use
  • Provide helpful error messages

The smaller pattern snippets below reuse the signing helpers and generated stubs shown above. They are partial integration patterns, not standalone programs.

The smaller pattern snippets below assume the same signing helpers shown in the complete Rust, Go, and Python examples above. If you call a protected RPC with only authorization metadata, Housecarl rejects it before authorization runs.

Common Integration Patterns

Pattern 1: API Gateway Authorization

Check authorization before routing requests to backend services.

use housecarl_lib::grpc::housecarl::{
    housecarl_service_client::HousecarlServiceClient, CheckAuthorizationRequest, RequestValue,
    request_value::Value as RequestValueKind,
};
use tonic::Request;
use tonic::transport::Channel;

fn single(value: impl Into<String>) -> RequestValue {
    RequestValue {
        value: Some(RequestValueKind::Single(value.into())),
    }
}

// Axum web framework example
async fn api_handler(
    Extension(authz_client): Extension<HousecarlServiceClient<Channel>>,
    user: AuthenticatedUser,
    Path(resource_path): Path<String>,
) -> Result<Json<Document>, StatusCode> {
    // Build authorization request
    let mut context = std::collections::HashMap::new();
    context.insert("subject".to_string(), single(user.username.clone()));
    context.insert("action".to_string(), single("read"));
    context.insert(
        "object".to_string(),
        single(format!(
            "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/{}",
            resource_path
        )),
    );
    for (k, v) in user.attributes() {
        context.insert(k, single(v));
    }

    let authz_req = CheckAuthorizationRequest { context };

    // authz_client should inject Authorization plus request-signing metadata
    // (Bearer token, signed-by, and date-filed-in)
    let response = authz_client
        .clone()
        .check_authorization(Request::new(authz_req))
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    if !response.into_inner().authorized {
        return Err(StatusCode::FORBIDDEN);
    }

    // Authorization passed - fetch and return resource
    let document = fetch_document(&resource_path).await?;
    Ok(Json(document))
}

Pattern 2: Service-to-Service Authorization

Service accounts making requests on behalf of users or systems.

// Billing service calling user service
func (s *BillingService) GetUserProfile(userID string) (*UserProfile, error) {
    // Check if billing service can access user profile
    single := func(val string) *pb.RequestValue {
        return &pb.RequestValue{Value: &pb.RequestValue_Single{Single: val}}
    }

    authzReq := &pb.CheckAuthorizationRequest{
        Context: map[string]*pb.RequestValue{
            "subject":  single("svc:billing-service"), // Service account
            "action":   single("read"),
            "object":   single(fmt.Sprintf("hc://domain/550e8400-e29b-41d4-a716-446655440000/users/%s/profile", userID)),
            "service":  single("billing"),
            "purpose":  single("invoice-generation"),
            "request_id": single(s.RequestID),
        },
    }

    // signedOutgoingContext should add authorization, signed-by, and date-filed-in
    ctx, err := signedOutgoingContext(
        context.Background(),
        authzReq,
        s.AuthzToken,
        s.AuthzSigningSecret,
    )
    if err != nil {
        return nil, fmt.Errorf("sign authz request: %w", err)
    }
    resp, err := s.authzClient.CheckAuthorization(ctx, authzReq)
    if err != nil {
        return nil, fmt.Errorf("authz check failed: %w", err)
    }

    if !resp.Authorized {
        return nil, fmt.Errorf("not authorized to access user profile")
    }

    // Proceed with fetching user profile
    return s.userServiceClient.GetProfile(userID)
}

Pattern 3: Batch Authorization Checks

Check multiple resources at once for efficiency.

def check_batch_authorization(user, action, resources, context=None):
    """
    Check authorization for multiple resources.
    Returns dict mapping resource to authorized/denied.
    """
    token = os.environ["HOUSECARL_TOKEN"]
    signing_secret = os.environ["HOUSECARL_SIGNING_SECRET"]
    channel = grpc.insecure_channel("localhost:50051")
    client = HousecarlServiceStub(channel)

    results = {}

    def single(value: str) -> RequestValue:
        return RequestValue(single=value)

    for resource in resources:
        ctx = {
            "subject": single(user),
            "action": single(action),
            "object": single(resource),
        }
        for key, value in (context or {}).items():
            ctx[key] = single(value)

        request = CheckAuthorizationRequest(context=ctx)
        body = request.SerializeToString()
        timestamp = int(time.time())
        time_bucket = timestamp // 300
        message = f"housecarl-request-v1:{len(body)}:{body.hex()}:{time_bucket}".encode("utf-8")
        signature = base64.b64encode(
            hmac.new(base64.b64decode(signing_secret), message, hashlib.sha256).digest()
        ).decode("utf-8")

        try:
            response = client.CheckAuthorization(
                request,
                metadata=[
                    ("authorization", f"Bearer {token}"),
                    ("signed-by", signature),
                    ("date-filed-in", str(timestamp)),
                ],
            )
            results[resource] = response.authorized
        except grpc.RpcError as e:
            print(f"Error checking {resource}: {e}")
            results[resource] = False  # Fail closed

    channel.close()
    return results

# Usage
resources_to_check = [
    "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/project-alpha/spec.pdf",
    "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/project-beta/roadmap.pdf",
    "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/shared/README.md",
]

results = check_batch_authorization(
    user="alice",
    action="read",
    resources=resources_to_check,
    context={"team": "engineering"}
)

# Filter to only allowed resources
allowed_resources = [r for r, allowed in results.items() if allowed]
print(f"Alice can read {len(allowed_resources)} of {len(resources_to_check)} documents")

Note: Consider implementing connection pooling for production batch operations to avoid creating a new connection for each request.

Pattern 4: Middleware Authorization

Implement authorization as middleware in your web framework.

// Axum middleware example
use housecarl_lib::grpc::housecarl::{
    housecarl_service_client::HousecarlServiceClient, CheckAuthorizationRequest, RequestValue,
    request_value::Value as RequestValueKind,
};
use tonic::Request;
use tonic::transport::Channel;

fn single(value: impl Into<String>) -> RequestValue {
    RequestValue {
        value: Some(RequestValueKind::Single(value.into())),
    }
}

pub async fn authz_middleware(
    Extension(authz_client): Extension<HousecarlServiceClient<Channel>>,
    user: AuthenticatedUser,
    req: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let path = req.uri().path();
    let method = req.method();

    // Map HTTP method to action
    let action = match method {
        &Method::GET => "read",
        &Method::POST => "create",
        &Method::PUT | &Method::PATCH => "update",
        &Method::DELETE => "delete",
        _ => return Err(StatusCode::METHOD_NOT_ALLOWED),
    };

    // Build resource URI from request path
    let resource_uri = format!("hc://domain/550e8400-e29b-41d4-a716-446655440000/api{}", path);

    // Check authorization
    let mut context = std::collections::HashMap::new();
    context.insert("subject".to_string(), single(user.username.clone()));
    context.insert("action".to_string(), single(action));
    context.insert("object".to_string(), single(resource_uri));
    for (k, v) in user.attributes() {
        context.insert(k, single(v));
    }

    let authz_req = CheckAuthorizationRequest { context };

    // authz_client should be a signed wrapper that injects authorization,
    // signed-by, and date-filed-in for each protected request.
    let response = authz_client
        .clone()
        .check_authorization(Request::new(authz_req))
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    if !response.into_inner().authorized {
        return Err(StatusCode::FORBIDDEN);
    }

    // Authorization passed - continue to handler
    Ok(next.run(req).await)
}

Error Handling

Always handle authorization errors gracefully:

Network Errors

// `client` here should be a signed authorization wrapper, not a raw tonic client.
match client.check_authorization(authz_req).await {
    Ok(response) => {
        if response.into_inner().authorized {
            // Proceed
        } else {
            // Denied
        }
    }
    Err(e) => {
        // Network error, timeout, or server unavailable
        log::error!("Authorization check failed: {}", e);

        // Decision: Fail open or fail closed?
        // Fail closed (recommended for security):
        return Err("Authorization service unavailable");

        // Fail open (only for non-critical paths):
        // log::warn!("Allowing request due to authz service unavailable");
        // proceed();
    }
}

Fail closed vs fail open:

  • Fail closed: Deny access when authz service is unavailable (more secure)
  • Fail open: Allow access when authz service is unavailable (better availability)

Recommendation: Fail closed for security-critical operations. Use retries and timeouts to minimize false denials.

Denied Requests

Provide helpful feedback when authorization is denied:

subject = "alice"
action = "read"
resource = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/report.pdf"

request = CheckAuthorizationRequest(
    context={
        "subject": RequestValue(single=subject),
        "action": RequestValue(single=action),
        "object": RequestValue(single=resource),
    }
)

metadata = build_signed_metadata(request.SerializeToString(), token, signing_secret)
response = client.CheckAuthorization(request, metadata=metadata)

if not response.authorized:
    # Log the denial for audit purposes
    logger.warning(
        f"Authorization denied: user={subject}, action={action}, object={resource}"
    )

    # Return user-friendly error
    raise PermissionError(
        "Access denied. Contact your administrator if you believe this is an error."
    )

Performance Optimization

Connection Pooling

Don't create a new connection for every authorization request:

// Create a shared client at application startup
type App struct {
    authzClient pb.HousecarlServiceClient
    authzConn   *grpc.ClientConn
}

func NewApp() (*App, error) {
    conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        return nil, err
    }

    return &App{
        authzClient: pb.NewHousecarlServiceClient(conn),
        authzConn:   conn,
    }, nil
}

func (a *App) Close() error {
    return a.authzConn.Close()
}

// Reuse the client for all requests. This helper should sign each request
// before calling the underlying gRPC client.
func (a *App) CheckAuthorization(ctx context.Context, req *pb.CheckAuthorizationRequest) (bool, error) {
    ctx, err := signedOutgoingContext(ctx, req, a.authzToken, a.authzSigningSecret)
    if err != nil {
        return false, err
    }
    resp, err := a.authzClient.CheckAuthorization(ctx, req)
    if err != nil {
        return false, err
    }
    return resp.Authorized, nil
}

Caching Authorization Decisions

For high-traffic applications, cache authorization decisions:

use std::collections::HashMap;
use std::time::{Duration, Instant};
use housecarl_lib::grpc::housecarl::{
    housecarl_service_client::HousecarlServiceClient, CheckAuthorizationRequest, RequestValue,
    request_value::Value as RequestValueKind,
};
use lru::LruCache;
use tonic::Request;
use tonic::transport::Channel;
use tokio::sync::Mutex;

fn single(value: impl Into<String>) -> RequestValue {
    RequestValue {
        value: Some(RequestValueKind::Single(value.into())),
    }
}

struct AuthzCache {
    cache: Mutex<LruCache<String, (bool, Instant)>>,
    ttl: Duration,
}

impl AuthzCache {
    fn cache_key(subject: &str, action: &str, object: &str) -> String {
        format!("{}:{}:{}", subject, action, object)
    }

    async fn check_with_cache(
        &self,
        client: &mut HousecarlServiceClient<Channel>,
        subject: &str,
        action: &str,
        object: &str,
        mut context: HashMap<String, RequestValue>,
    ) -> Result<bool, Box<dyn std::error::Error>> {
        let key = Self::cache_key(subject, action, object);

        // Check cache
        {
            let mut cache = self.cache.lock().await;
            if let Some((allowed, cached_at)) = cache.get(&key) {
                if cached_at.elapsed() < self.ttl {
                    return Ok(*allowed);
                }
            }
        }

        // Cache miss - make real request
        context.insert("subject".to_string(), single(subject));
        context.insert("action".to_string(), single(action));
        context.insert("object".to_string(), single(object));
        let req = CheckAuthorizationRequest { context };
        // `client` should be a signed wrapper that injects request metadata.
        let response = client.check_authorization(Request::new(req)).await?;
        let allowed = response.into_inner().authorized;

        // Update cache
        {
            let mut cache = self.cache.lock().await;
            cache.put(key, (allowed, Instant::now()));
        }

        Ok(allowed)
    }
}

Cache considerations:

  • TTL: Keep it short (1-5 minutes) to avoid stale decisions
  • Invalidation: Invalidate cache when policies change
  • Size: Limit cache size to prevent memory issues
  • Security: Caching "allow" is safer than caching "deny"

When to cache:

  • High-traffic read operations
  • Stable authorization decisions
  • Non-critical resources

When NOT to cache:

  • Security-critical decisions
  • Rapidly changing policies
  • User attribute changes (deactivation, role changes)

Request Timeouts

Always set timeouts for authorization requests:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

ctx, err = signedOutgoingContext(ctx, req, token, signingSecret)
if err != nil {
    return false, err
}
resp, err := client.CheckAuthorization(ctx, req)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        log.Error("Authorization request timed out")
        // Fail closed
        return false, err
    }
    return false, err
}

Testing Authorization Integration

Unit Testing

Mock the authorization client in your tests:

#[cfg(test)]
mod tests {
    use super::*;
    use housecarl_lib::grpc::housecarl::{CheckAuthorizationRequest, CheckAuthorizationResponse};
    use mockall::predicate::*;
    use mockall::mock;

    mock! {
        AuthzClient {}

        impl AuthzClient {
            async fn check_authorization(
                &self,
                req: CheckAuthorizationRequest,
            ) -> Result<CheckAuthorizationResponse, Error>;
        }
    }

    #[tokio::test]
    async fn test_authorized_request() {
        let mut mock_client = MockAuthzClient::new();
        mock_client
            .expect_check_authorization()
            .with(predicate::always())
            .returning(|_| Ok(CheckAuthorizationResponse { authorized: true }));

        let result = handle_request(mock_client, "alice", "read", "doc123").await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_denied_request() {
        let mut mock_client = MockAuthzClient::new();
        mock_client
            .expect_check_authorization()
            .returning(|_| Ok(CheckAuthorizationResponse { authorized: false }));

        let result = handle_request(mock_client, "alice", "write", "doc123").await;
        assert!(result.is_err());
    }
}

Integration Testing

Test against a real Housecarl instance with known policies:

import unittest
from authorization import check_authorization

class TestAuthorization(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # Set up test tenant with known policies
        # (Assumes test housecarl instance)
        setup_test_policies()

    def test_engineer_can_read_docs(self):
        """Engineers should be able to read engineering docs"""
        result = check_authorization(
            user="test-engineer",
            action="read",
            resource="hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/engineering/spec.pdf",
            context={"team": "engineering"}
        )
        self.assertTrue(result)

    def test_contractor_cannot_deploy(self):
        """Contractors should not be able to deploy to production"""
        result = check_authorization(
            user="test-contractor",
            action="deploy",
            resource="hc://domain/550e8400-e29b-41d4-a716-446655440000/environments/production",
            context={"contract_type": "contractor"}
        )
        self.assertFalse(result)

Troubleshooting

"Authorization always denied"

Check:

  1. Is the policy actually deployed? housectl domain list-policies <domain>
  2. Does the resource URI match the policy pattern exactly?
  3. Are context attributes matching policy requirements?
  4. Is the user authenticated with the correct tenant?

Debug:

# Test locally with your request
housectl authz can-i test-request.json

# See which policies match
housectl authz can-i-local --request test-request.json policy.toml

"Connection refused"

Check:

  1. Is the Housecarl server running?
  2. Is the endpoint correct? Check the host and port.
  3. Is there a firewall blocking gRPC traffic?

Debug:

# Reflection often requires auth even for `list`
grpcurl -plaintext -H "authorization: Bearer ${HOUSECARL_TOKEN}" localhost:50051 list

# Check health
housectl config health

Protected RPCs such as CheckAuthorization also need authorization, signed-by, and date-filed-in metadata. If grpcurl list works but your authorization request still fails, compare your client against the signed Rust example above.

"Timeout"

Check:

  1. Is the timeout too short? gRPC calls typically take 1-10ms.
  2. Is the server overloaded or slow?
  3. Network latency issues?

Debug: Increase timeout temporarily to see if it's a timeout or a hang:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)

Summary

Key Takeaways:

  • Authorization requests have four parts: subject, action, object, context
  • Use gRPC clients for your language (Rust, Go, Python, etc.)
  • Always handle errors (network, denied, timeout)
  • Fail closed for security, fail open only for non-critical paths
  • Use connection pooling and caching for performance
  • Test with mocks (unit) and real instances (integration)

Next Steps:

Ready to integrate Housecarl into your application? Start with the Quick Start and come back here when you're ready to integrate!