Skip to content

Latest commit

 

History

History
659 lines (504 loc) · 13.6 KB

admin-api.md

File metadata and controls

659 lines (504 loc) · 13.6 KB

Admin API Guide

The Admin API provides programmatic access to manage licenses, organizations, and system configuration. This guide covers authentication, all available endpoints, and security best practices.

Security Warning: The Admin API provides full control over your licensing system. Always protect it with:

  • JWT authentication (required in production)
  • IP whitelisting (configure in your reverse proxy)
  • TLS/HTTPS encryption
  • Strong, rotated credentials

Table of Contents


Enabling the Admin API

The Admin API requires the admin-api feature flag:

# Cargo.toml
[dependencies]
talos = { git = "https://github.com/dmriding/talos", features = ["admin-api"] }

For production, also enable JWT authentication:

talos = { git = "https://github.com/dmriding/talos", features = ["admin-api", "jwt-auth"] }

Authentication

JWT Authentication (Recommended)

When jwt-auth is enabled, all admin endpoints require a valid JWT token.

Configuration:

# config.toml
[auth]
enabled = true
jwt_issuer = "talos"
jwt_audience = "talos-api"
token_expiration_secs = 86400  # 24 hours
# .env
TALOS_JWT_SECRET=your-secret-key-at-least-32-characters-long

Creating a Token:

# Use the bootstrap token endpoint (first-time setup)
curl -X POST http://localhost:8080/api/v1/tokens \
  -H "Content-Type: application/json" \
  -d '{
    "name": "admin-service",
    "scopes": ["licenses:*", "tokens:*"]
  }'

Using the Token:

curl -X GET http://localhost:8080/api/v1/licenses \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Scopes

Tokens are scoped to limit access:

Scope Description
licenses:read Read license information
licenses:write Create, update licenses
licenses:delete Revoke, blacklist licenses
licenses:* All license operations
tokens:read Read token information
tokens:write Create tokens
tokens:delete Revoke tokens
tokens:* All token operations
* Full access (admin)

Security Best Practices

IP Whitelisting

Critical: Restrict Admin API access to trusted IP addresses.

Built-in IP Whitelist (Recommended)

Talos has built-in IP whitelisting that applies to all admin endpoints:

# config.toml
[admin]
# Only allow admin API access from these IPs/networks
ip_whitelist = ["127.0.0.1", "::1", "10.0.0.0/8", "192.168.0.0/16"]
audit_logging = false

Features:

  • Supports individual IPs (192.168.1.100)
  • Supports CIDR notation (10.0.0.0/8, 192.168.0.0/16)
  • Supports IPv4 and IPv6
  • Empty list = allow all IPs (disabled)
  • Checks X-Forwarded-For and X-Real-IP headers when behind a proxy

Common configurations:

# Localhost only
ip_whitelist = ["127.0.0.1", "::1"]

# Internal network
ip_whitelist = ["10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"]

# Specific servers
ip_whitelist = ["10.0.1.5", "10.0.1.6", "192.168.1.100"]

Additional Protection with Reverse Proxy

For defense in depth, also configure IP restrictions at your reverse proxy:

nginx:

location /api/v1/ {
    # Only allow from internal network
    allow 10.0.0.0/8;
    allow 192.168.0.0/16;
    allow 127.0.0.1;
    deny all;

    proxy_pass http://talos;
}

# Client endpoints are public
location /api/v1/client/ {
    proxy_pass http://talos;
}

Traefik:

http:
  middlewares:
    admin-whitelist:
      ipWhiteList:
        sourceRange:
          - "10.0.0.0/8"
          - "192.168.0.0/16"
          - "127.0.0.1"

  routers:
    talos-admin:
      rule: "Host(`license.example.com`) && PathPrefix(`/api/v1/`) && !PathPrefix(`/api/v1/client/`)"
      middlewares:
        - admin-whitelist

Additional Security Measures

  1. Use HTTPS - Never expose Admin API over plain HTTP
  2. Rotate secrets - Change JWT secret periodically
  3. Audit logs - Monitor all admin actions
  4. Least privilege - Create tokens with minimal required scopes
  5. Short-lived tokens - Use short expiration times for automated systems

License Management

Create License

Creates a new license with optional organization, features, and tier.

POST /api/v1/licenses
Content-Type: application/json
Authorization: Bearer <token>

{
  "org_id": "acme-corp",
  "org_name": "Acme Corporation",
  "tier": "pro",
  "features": ["export", "api_access"],
  "expires_at": "2025-12-31T23:59:59Z",
  "metadata": {
    "stripe_customer_id": "cus_123",
    "plan": "annual"
  }
}

Response:

{
  "license_id": "550e8400-e29b-41d4-a716-446655440000",
  "license_key": "LIC-A1B2-C3D4-E5F6-G7H8",
  "org_id": "acme-corp",
  "org_name": "Acme Corporation",
  "tier": "pro",
  "features": ["export", "api_access"],
  "status": "active",
  "expires_at": "2025-12-31T23:59:59Z",
  "created_at": "2024-01-15T10:30:00Z",
  "is_bound": false
}

Notes:

  • license_key is auto-generated using configured prefix
  • features can be explicit or derived from tier configuration
  • metadata is stored as JSON and returned in responses

Batch Create Licenses

Create multiple licenses at once (up to 1000).

POST /api/v1/licenses/batch
Content-Type: application/json
Authorization: Bearer <token>

{
  "count": 100,
  "org_id": "acme-corp",
  "tier": "pro",
  "expires_at": "2025-12-31T23:59:59Z"
}

Response:

{
  "created": 100,
  "licenses": [
    {
      "license_id": "...",
      "license_key": "LIC-XXXX-YYYY-ZZZZ-AAAA"
    },
    ...
  ]
}

Get License

Retrieve a license by ID.

GET /api/v1/licenses/{license_id}
Authorization: Bearer <token>

Response:

{
  "license_id": "550e8400-e29b-41d4-a716-446655440000",
  "license_key": "LIC-A1B2-C3D4-E5F6-G7H8",
  "org_id": "acme-corp",
  "org_name": "Acme Corporation",
  "tier": "pro",
  "features": ["export", "api_access"],
  "status": "active",
  "expires_at": "2025-12-31T23:59:59Z",
  "is_bound": true,
  "hardware_id": "abc123...",
  "device_name": "John's Workstation",
  "bound_at": "2024-01-15T14:00:00Z",
  "last_seen_at": "2024-01-16T09:30:00Z"
}

List Licenses

List licenses with filtering and pagination.

GET /api/v1/licenses?org_id=acme-corp&page=1&per_page=20
Authorization: Bearer <token>

Query Parameters:

Parameter Description Default
org_id Filter by organization -
status Filter by status (active, suspended, revoked) -
page Page number 1
per_page Items per page (max 100) 20

Response:

{
  "licenses": [...],
  "total": 150,
  "page": 1,
  "per_page": 20,
  "total_pages": 8
}

Update License

Update license properties.

PATCH /api/v1/licenses/{license_id}
Content-Type: application/json
Authorization: Bearer <token>

{
  "tier": "enterprise",
  "features": ["export", "api_access", "sso"],
  "expires_at": "2026-12-31T23:59:59Z",
  "metadata": {
    "upgraded_at": "2024-01-16"
  }
}

Notes:

  • Only specified fields are updated
  • Changing tier can auto-update features (if tier config exists)

Organization Management

List Organization Licenses

GET /api/v1/licenses?org_id=acme-corp
Authorization: Bearer <token>

Organization Summary (Custom Implementation)

You can build organization summaries using the list endpoint:

# Example: Python script to get org summary
import requests

response = requests.get(
    "https://license.example.com/api/v1/licenses",
    params={"org_id": "acme-corp", "per_page": 100},
    headers={"Authorization": f"Bearer {token}"}
)

licenses = response.json()["licenses"]
active = sum(1 for l in licenses if l["status"] == "active")
bound = sum(1 for l in licenses if l["is_bound"])

print(f"Organization: acme-corp")
print(f"Total licenses: {len(licenses)}")
print(f"Active: {active}")
print(f"Bound: {bound}")

License Lifecycle

Suspend License

Temporarily suspend a license (with optional grace period).

POST /api/v1/licenses/{license_id}/suspend
Content-Type: application/json
Authorization: Bearer <token>

{
  "reason": "Payment failed",
  "grace_period_days": 7,
  "message": "Please update your payment method"
}

Notes:

  • Suspended licenses can still be used during grace period
  • After grace period, license is automatically revoked (if background-jobs enabled)

Revoke License

Permanently revoke a license.

POST /api/v1/licenses/{license_id}/revoke
Content-Type: application/json
Authorization: Bearer <token>

{
  "reason": "Terms of service violation"
}

Notes:

  • Clears hardware binding
  • Cannot be used again without reinstatement

Reinstate License

Restore a suspended or revoked license.

POST /api/v1/licenses/{license_id}/reinstate
Content-Type: application/json
Authorization: Bearer <token>

{
  "new_expires_at": "2025-12-31T23:59:59Z",
  "reset_bandwidth": true
}

Notes:

  • Cannot reinstate blacklisted licenses
  • Optionally set new expiration and reset usage

Extend License

Extend the expiration date.

POST /api/v1/licenses/{license_id}/extend
Content-Type: application/json
Authorization: Bearer <token>

{
  "new_expires_at": "2026-12-31T23:59:59Z",
  "reset_bandwidth": false
}

Release Hardware Binding

Force-release a license from its current hardware (admin action).

POST /api/v1/licenses/{license_id}/release
Content-Type: application/json
Authorization: Bearer <token>

{
  "reason": "User requested transfer to new machine"
}

Notes:

  • Records admin release in binding history
  • License can then be bound to a different machine

Blacklist License

Permanently ban a license (cannot be reinstated).

POST /api/v1/licenses/{license_id}/blacklist
Content-Type: application/json
Authorization: Bearer <token>

{
  "reason": "License sharing detected"
}

Notes:

  • Sets is_blacklisted = true
  • Cannot be reinstated through normal means
  • Use for fraud, abuse, or policy violations

Update Usage

Update bandwidth/quota usage (for metered licenses).

PATCH /api/v1/licenses/{license_id}/usage
Content-Type: application/json
Authorization: Bearer <token>

{
  "bandwidth_used_bytes": 1073741824,
  "reset": false
}

Response:

{
  "license_id": "...",
  "bandwidth_used_bytes": 1073741824,
  "bandwidth_limit_bytes": 107374182400,
  "quota_exceeded": false,
  "usage_percentage": 1.0
}

Token Management

Create Token

Create a new API token.

POST /api/v1/tokens
Content-Type: application/json
Authorization: Bearer <token>

{
  "name": "billing-service",
  "scopes": ["licenses:read", "licenses:write"],
  "expires_in_days": 365
}

Response:

{
  "token_id": "tok_123",
  "name": "billing-service",
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "scopes": ["licenses:read", "licenses:write"],
  "expires_at": "2025-01-15T10:30:00Z"
}

Important: The token value is only shown once. Store it securely!

List Tokens

GET /api/v1/tokens
Authorization: Bearer <token>

Response:

{
  "tokens": [
    {
      "token_id": "tok_123",
      "name": "billing-service",
      "scopes": ["licenses:read", "licenses:write"],
      "created_at": "2024-01-15T10:30:00Z",
      "expires_at": "2025-01-15T10:30:00Z",
      "last_used_at": "2024-01-16T09:00:00Z"
    }
  ]
}

Revoke Token

DELETE /api/v1/tokens/{token_id}
Authorization: Bearer <token>

Error Handling

All errors return a standard format:

{
  "error": {
    "code": "LICENSE_NOT_FOUND",
    "message": "The requested license does not exist",
    "details": null
  }
}

Error Codes

Code HTTP Status Description
LICENSE_NOT_FOUND 404 License doesn't exist
LICENSE_EXPIRED 403 License has expired
LICENSE_REVOKED 403 License has been revoked
LICENSE_SUSPENDED 403 License is suspended
LICENSE_BLACKLISTED 403 License is blacklisted
ALREADY_BOUND 409 Already bound to another device
NOT_BOUND 409 Not bound to any device
INVALID_REQUEST 400 Request validation failed
MISSING_FIELD 400 Required field missing
MISSING_TOKEN 401 No auth token provided
INVALID_TOKEN 401 Token is invalid
TOKEN_EXPIRED 401 Token has expired
INSUFFICIENT_SCOPE 403 Token lacks required scope
DATABASE_ERROR 500 Database operation failed
INTERNAL_ERROR 500 Unexpected server error

Example Error Handling

import requests

response = requests.post(
    "https://license.example.com/api/v1/licenses",
    json={"org_id": "acme"},
    headers={"Authorization": f"Bearer {token}"}
)

if not response.ok:
    error = response.json().get("error", {})
    code = error.get("code")
    message = error.get("message")

    if code == "MISSING_TOKEN":
        print("Authentication required")
    elif code == "INSUFFICIENT_SCOPE":
        print("Token doesn't have permission for this action")
    elif code == "INVALID_REQUEST":
        print(f"Invalid request: {message}")
    else:
        print(f"Error: {message}")

Next Steps