PRE-RELEASE INFORMATION: SUBJECT TO CHANGE

Housecarl API Overview

This document covers the REST API for housecarl authorization. For gRPC API documentation, see the API Reference.

REST API Endpoints

Housecarl provides a simple REST API for authorization checks. This is ideal for applications that want a lightweight HTTP interface without the overhead of gRPC.

Base URL

https://your-housecarl-instance.com

For local development:

http://localhost:1234

CheckAuthorization - POST /v1/authz/check

The primary authorization endpoint. Use this to ask "can this subject perform this action on this object?"

Endpoint

POST /v1/authz/check

Authentication

JWT Bearer Token (Currently Supported)

Include your JWT token in the Authorization header:

Authorization: Bearer <your-jwt-token>

You can obtain a JWT token by logging in via the Login gRPC endpoint or using housectl config login.

API Keys (gRPC Only)

Note: API keys are currently only supported for gRPC calls, not REST endpoints. If you need API key authentication for REST, please use the gRPC API or request this feature.

Request Format

The request body must be JSON with a context object containing three required keys:

{
  "context": {
    "subject": "user:alice",
    "action": "read",
    "object": "hc://550e8400-e29b-41d4-a716-446655440000/documents/report.pdf"
  }
}

Required Context Fields

FieldTypeDescriptionExample
subjectstringThe principal attempting the action (who)"user:alice", "service:billing-api"
actionstringThe action being attempted (what)"read", "write", "delete"
objectstringThe resource being accessed (where) - must be a housecarl URL"hc://tenant-uuid/documents/file.txt"

Optional Context Fields

You can include additional context fields for policy evaluation:

{
  "context": {
    "subject": "user:alice",
    "action": "read",
    "object": "hc://550e8400-e29b-41d4-a716-446655440000/documents/report.pdf",
    "department": "engineering",
    "time_of_day": "business_hours",
    "ip_address": "192.168.1.100"
  }
}

These additional fields can be used in policy rules to make authorization decisions based on attributes.

Multi-valued Attributes

Context values can be arrays of strings:

{
  "context": {
    "subject": "user:alice",
    "action": "read",
    "object": "hc://550e8400-e29b-41d4-a716-446655440000/documents/report.pdf",
    "group": ["engineering", "managers", "security-cleared"]
  }
}

Response Format

Success (200 OK)

{
  "allowed": true
}

or

{
  "allowed": false
}

Error Responses

All errors follow this format:

{
  "error": "error_code",
  "message": "Human-readable error description"
}
Status CodeError CodeDescription
400 Bad Requestinvalid_requestMissing required fields or invalid JSON
401 UnauthorizedunauthorizedMissing or invalid JWT token
403 ForbiddenforbiddenJWT lacks tenant context
429 Too Many Requestsrate_limitedRate limit exceeded
500 Internal Server Errorinternal_errorServer-side error

Complete Examples

Rust Example

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use ureq;

#[derive(Serialize)]
struct CheckAuthorizationRequest {
    context: HashMap<String, serde_json::Value>,
}

#[derive(Deserialize)]
struct CheckAuthorizationResponse {
    allowed: bool,
}

fn check_authorization(
    base_url: &str,
    jwt_token: &str,
    subject: &str,
    action: &str,
    object: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
    let mut context = HashMap::new();
    context.insert("subject".to_string(), serde_json::Value::String(subject.to_string()));
    context.insert("action".to_string(), serde_json::Value::String(action.to_string()));
    context.insert("object".to_string(), serde_json::Value::String(object.to_string()));

    let request = CheckAuthorizationRequest { context };

    let response = ureq::post(&format!("{}/v1/authz/check", base_url))
        .header("Authorization", &format!("Bearer {}", jwt_token))
        .header("Content-Type", "application/json")
        .send_json(&request)?;

    let response_text = response.into_body().read_to_string()?;

    let check_response: CheckAuthorizationResponse = serde_json::from_str(&response_text)?;
    Ok(check_response.allowed)
}

// Usage example
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let base_url = "http://localhost:1234";
    let jwt_token = "eyJhbGc..."; // Your JWT token from login

    let allowed = check_authorization(
        base_url,
        jwt_token,
        "user:alice",
        "read",
        "hc://550e8400-e29b-41d4-a716-446655440000/documents/report.pdf"
    )?;

    if allowed {
        println!("Access granted");
    } else {
        println!("Access denied");
    }

    Ok(())
}

Go Example

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

type CheckAuthorizationRequest struct {
    Context map[string]interface{} `json:"context"`
}

type CheckAuthorizationResponse struct {
    Allowed bool `json:"allowed"`
}

type ErrorResponse struct {
    Error   string `json:"error"`
    Message string `json:"message"`
}

func CheckAuthorization(baseURL, jwtToken, subject, action, object string) (bool, error) {
    // Build request
    reqBody := CheckAuthorizationRequest{
        Context: map[string]interface{}{
            "subject": subject,
            "action":  action,
            "object":  object,
        },
    }

    jsonData, err := json.Marshal(reqBody)
    if err != nil {
        return false, fmt.Errorf("failed to marshal request: %w", err)
    }

    // Create HTTP request
    req, err := http.NewRequest("POST", baseURL+"/v1/authz/check", bytes.NewBuffer(jsonData))
    if err != nil {
        return false, fmt.Errorf("failed to create request: %w", err)
    }

    req.Header.Set("Authorization", "Bearer "+jwtToken)
    req.Header.Set("Content-Type", "application/json")

    // Send request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return false, fmt.Errorf("failed to send request: %w", err)
    }
    defer resp.Body.Close()

    // Read response
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return false, fmt.Errorf("failed to read response: %w", err)
    }

    // Handle error responses
    if resp.StatusCode != http.StatusOK {
        var errResp ErrorResponse
        if err := json.Unmarshal(body, &errResp); err != nil {
            return false, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
        }
        return false, fmt.Errorf("%s: %s", errResp.Error, errResp.Message)
    }

    // Parse success response
    var checkResp CheckAuthorizationResponse
    if err := json.Unmarshal(body, &checkResp); err != nil {
        return false, fmt.Errorf("failed to unmarshal response: %w", err)
    }

    return checkResp.Allowed, nil
}

func main() {
    baseURL := "http://localhost:1234"
    jwtToken := "eyJhbGc..." // Your JWT token from login

    allowed, err := CheckAuthorization(
        baseURL,
        jwtToken,
        "user:alice",
        "read",
        "hc://550e8400-e29b-41d4-a716-446655440000/documents/report.pdf",
    )
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    if allowed {
        fmt.Println("Access granted")
    } else {
        fmt.Println("Access denied")
    }
}

Python Example

import requests
from typing import Dict, Any

class HousecarlClient:
    def __init__(self, base_url: str, jwt_token: str):
        self.base_url = base_url
        self.jwt_token = jwt_token
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {jwt_token}',
            'Content-Type': 'application/json'
        })

    def check_authorization(
        self,
        subject: str,
        action: str,
        object_url: str,
        extra_context: Dict[str, Any] = None
    ) -> bool:
        """
        Check if a subject can perform an action on an object.

        Args:
            subject: The principal (e.g., "user:alice")
            action: The action (e.g., "read", "write")
            object_url: The housecarl resource URL (e.g., "hc://tenant-id/resource")
            extra_context: Optional additional context for policy evaluation

        Returns:
            True if authorized, False if denied

        Raises:
            requests.HTTPError: On error responses
        """
        context = {
            "subject": subject,
            "action": action,
            "object": object_url
        }

        # Add extra context if provided
        if extra_context:
            context.update(extra_context)

        request_body = {"context": context}

        response = self.session.post(
            f"{self.base_url}/v1/authz/check",
            json=request_body
        )

        # Raise exception on error status codes
        response.raise_for_status()

        result = response.json()
        return result.get("allowed", False)


# Usage example
if __name__ == "__main__":
    base_url = "http://localhost:1234"
    jwt_token = "eyJhbGc..."  # Your JWT token from login

    client = HousecarlClient(base_url, jwt_token)

    try:
        allowed = client.check_authorization(
            subject="user:alice",
            action="read",
            object_url="hc://550e8400-e29b-41d4-a716-446655440000/documents/report.pdf"
        )

        if allowed:
            print("Access granted")
        else:
            print("Access denied")

    except requests.HTTPError as e:
        print(f"Error: {e}")
        if e.response is not None:
            error_body = e.response.json()
            print(f"Error code: {error_body.get('error')}")
            print(f"Message: {error_body.get('message')}")

curl Example

# Simple authorization check
curl -X POST http://localhost:1234/v1/authz/check \
  -H "Authorization: Bearer eyJhbGc..." \
  -H "Content-Type: application/json" \
  -d '{
    "context": {
      "subject": "user:alice",
      "action": "read",
      "object": "hc://550e8400-e29b-41d4-a716-446655440000/documents/report.pdf"
    }
  }'

# Response: {"allowed":true}

# With additional context attributes
curl -X POST http://localhost:1234/v1/authz/check \
  -H "Authorization: Bearer eyJhbGc..." \
  -H "Content-Type: application/json" \
  -d '{
    "context": {
      "subject": "user:alice",
      "action": "read",
      "object": "hc://550e8400-e29b-41d4-a716-446655440000/documents/report.pdf",
      "department": "engineering",
      "group": ["developers", "full-time"]
    }
  }'

# Response: {"allowed":true}

How to Get a JWT Token

To use the REST API, you need a JWT token. Here are the common ways to obtain one:

Option 1: Using housectl CLI

The easiest way for development:

# Login to get a JWT
housectl config login --url http://localhost:1234 --username alice --tenant my-company

# Check your token (stored in ~/.config/housecarl.toml)
housectl config status

The token is stored in your config file and automatically used by housectl. To extract it for use in your application:

# On Linux/Mac
grep token ~/.config/housecarl.toml

Option 2: Using the gRPC Login Endpoint

For programmatic access:

  1. Call the Login gRPC method with username and password
  2. Receive JWT tokens in the response
  3. Use the access token for authorization checks

See the API Reference for details on the Login method.

For production services, create a service account and use its credentials:

# Create a service account
housectl tenant create-service-account my-service "Service account for billing API"

# Get an API key for the service account
housectl admin create-api-key --service-account <sa-id> --name "billing-api-key"

Note: API keys currently work with gRPC only. For REST, you'll need to exchange the service account credentials for a JWT token.

Rate Limiting

The /v1/authz/check endpoint is rate-limited per tenant to prevent abuse. If you exceed the rate limit, you'll receive a 429 Too Many Requests response:

{
  "error": "rate_limited",
  "message": "rate limit exceeded: burst limit of 1000 requests per 100ms exceeded"
}

Rate Limit Details:

  • Burst limit: 1000 requests per 100ms per tenant
  • Sustained rate: Determined by your tenant's token quotas

Best Practices:

  • Cache authorization decisions when appropriate
  • Use batch checks if available
  • Implement exponential backoff on rate limit errors

Error Handling Best Practices

Always handle errors gracefully:

try:
    allowed = client.check_authorization(subject, action, object_url)
    if allowed:
        # Perform the action
        pass
    else:
        # Deny access
        return 403
except requests.HTTPError as e:
    if e.response.status_code == 401:
        # Token expired or invalid - re-authenticate
        refresh_jwt_token()
    elif e.response.status_code == 429:
        # Rate limited - back off and retry
        time.sleep(1)
        retry_with_backoff()
    else:
        # Other errors - fail securely (deny access)
        return 500

Observability

All authorization checks are logged to the audit service and include:

  • Subject, action, and object
  • Authorization decision (allow/deny)
  • Timestamp
  • Requesting user and tenant

You can query audit logs via the audit service API or UI.

Performance Considerations

  • Latency: Expect 10-50ms latency for authorization checks (depending on policy complexity)
  • Caching: Consider caching results for frequently-checked permissions (with short TTL)
  • Connection Pooling: Reuse HTTP connections for better performance

Next Steps


Requirements for API use

housectl use

API routes of usual use