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
CheckAuthorizationRequestwith acontextmap ofRequestValues - Include the Bearer token plus
signed-byanddate-filed-inmetadata - Check
response.authorizedbefore 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:
- Is the policy actually deployed?
housectl domain list-policies <domain> - Does the resource URI match the policy pattern exactly?
- Are context attributes matching policy requirements?
- 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:
- Is the Housecarl server running?
- Is the endpoint correct? Check the host and port.
- 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:
- Is the timeout too short? gRPC calls typically take 1-10ms.
- Is the server overloaded or slow?
- 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:
- See complete API: API Reference
- Understand policies: Policy Administration
- Production patterns: Developer Cookbook
- Real examples: Code Examples
Ready to integrate Housecarl into your application? Start with the Quick Start and come back here when you're ready to integrate!