This document covers the REST API for housecarl authorization. For gRPC API documentation, see the API Reference.
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.
https://your-housecarl-instance.com
For local development:
http://localhost:1234
The primary authorization endpoint. Use this to ask "can this subject perform this action on this object?"
POST /v1/authz/check
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.
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"
}
}
| Field | Type | Description | Example |
|---|---|---|---|
subject | string | The principal attempting the action (who) | "user:alice", "service:billing-api" |
action | string | The action being attempted (what) | "read", "write", "delete" |
object | string | The resource being accessed (where) - must be a housecarl URL | "hc://tenant-uuid/documents/file.txt" |
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.
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"]
}
}
Success (200 OK)
{
"allowed": true
}
or
{
"allowed": false
}
Error Responses
All errors follow this format:
{
"error": "error_code",
"message": "Human-readable error description"
}
| Status Code | Error Code | Description |
|---|---|---|
| 400 Bad Request | invalid_request | Missing required fields or invalid JSON |
| 401 Unauthorized | unauthorized | Missing or invalid JWT token |
| 403 Forbidden | forbidden | JWT lacks tenant context |
| 429 Too Many Requests | rate_limited | Rate limit exceeded |
| 500 Internal Server Error | internal_error | Server-side error |
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(())
}
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")
}
}
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')}")
# 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}
To use the REST API, you need a JWT token. Here are the common ways to obtain one:
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
For programmatic access:
Login gRPC method with username and passwordSee 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.
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:
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
All authorization checks are logged to the audit service and include:
You can query audit logs via the audit service API or UI.