adminapi

package
v1.10.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 5, 2026 License: Apache-2.0 Imports: 55 Imported by: 0

Documentation

Overview

Package adminapi exposes a read-only admin HTTP surface over the fabriq query facade. It is designed for consumption by the fabriq-admin SPA and any operator tooling that needs a stable, auth-agnostic read API over the data fabric.

The extension is auth-agnostic: the host MUST attach authentication and tenant-injection middleware via WithRouteOptions. Fabriq adminapi adds no auth of its own so that the host controls the security boundary entirely.

Phase 1 endpoints:

GET {BasePath}/meta            → service metadata + capabilities
GET {BasePath}/entities        → paginated entity list (requires ?type=)
GET {BasePath}/entities/{id}   → single entity by type and id (requires ?type=)
GET {BasePath}/capabilities    → fabric subsystem capabilities (instance-level,
                                 or per-type with ?type=)

Index

Constants

View Source
const APIVersion = 1

APIVersion is the current major version of the adminapi wire contract. Clients may advertise the version they were built against via the X-Fabriq-Api-Version request header; the auth middleware rejects requests whose advertised major differs from this value (HTTP 426). Responses always carry the current APIVersion in the same header.

View Source
const PluginRemoteEntityType = "admin_plugin_remote"

PluginRemoteEntityType is the well-known dynamic entity type name for remote plugin specs. The host application must register a matching EntitySpec (with a DynamicSchema) before using these endpoints; see PluginRemoteSpec.

View Source
const Version = forgeext.Version

Version is the adminapi extension version.

Variables

View Source
var ErrKeyNotFound = errors.New("adminapi: api key not found")

ErrKeyNotFound is returned by KeyStore.Revoke when no key has the given id.

Functions

func PluginRemoteSpec

func PluginRemoteSpec() registry.EntitySpec

PluginRemoteSpec returns the canonical EntitySpec for admin_plugin_remote. The host application calls reg.Register(adminapi.PluginRemoteSpec()) during startup so the entity is available to the command plane and the relational querier. In unit tests, register it via buildPluginWorld or equivalent.

Types

type Authorizer added in v0.0.6

type Authorizer interface {
	Authorize(ctx context.Context, capability string) (allowed bool, err error)
}

Authorizer decides whether the current request may exercise a named capability. ctx is the request context — it carries the resolved tenant and API-key id, plus any principal a host authz middleware (e.g. warden) stamped before the handler ran. A resource-free (ctx, capability) shape is the minimal common denominator: the tenant is already in ctx, so a per-tenant authz system reads it from there.

type AuthorizerFunc added in v0.0.6

type AuthorizerFunc func(ctx context.Context, capability string) (bool, error)

AuthorizerFunc adapts a plain function to Authorizer.

func (AuthorizerFunc) Authorize added in v0.0.6

func (f AuthorizerFunc) Authorize(ctx context.Context, capability string) (bool, error)

Authorize implements Authorizer.

type Extension

type Extension struct {
	forge.BaseExtension
	// contains filtered or unexported fields
}

Extension exposes the fabriq data fabric as a read-only admin HTTP surface. It depends on the "fabriq" extension and resolves its query facade at Start.

func NewAdminAPI

func NewAdminAPI(fab *forgeext.Extension, opts ...Option) *Extension

NewAdminAPI builds the adminapi extension wired to a started fabriq Extension. The endpoint is auth-agnostic: the host MUST attach authentication and tenant-injection middleware via WithRouteOptions.

func (*Extension) Dependencies

func (e *Extension) Dependencies() []string

Dependencies implements forge.Extension.

func (*Extension) Description

func (e *Extension) Description() string

Description implements forge.Extension.

func (*Extension) Name

func (e *Extension) Name() string

Name implements forge.Extension.

func (*Extension) Ready added in v1.9.0

func (e *Extension) Ready(_ context.Context) error

Ready reports whether Start has completed. The admin API is ready to serve once it has resolved the fabriq facade and bootstrapped its admin key.

func (*Extension) Register

func (e *Extension) Register(app forge.App) error

Register registers the admin controller routes; the fabric resolves lazily in Start.

func (*Extension) Start

func (e *Extension) Start(ctx context.Context) error

Start resolves the fabriq facade from the started fabriq extension.

func (*Extension) Stop

func (e *Extension) Stop(_ context.Context) error

Stop stops the extension.

func (*Extension) Version

func (e *Extension) Version() string

Version implements forge.Extension.

type IssuedKey

type IssuedKey struct {
	ID     string
	Prefix string
	Key    string
}

IssuedKey is returned once, at issue time. Key is the plaintext bearer token and is never persisted or recoverable afterwards — only its hash is stored.

type KeyRecord

type KeyRecord struct {
	ID            string
	Prefix        string
	TenantID      string
	Label         string
	CanManageKeys bool
	CreatedAt     time.Time
	RevokedAt     *time.Time
	// ExpiresAt is nil for keys that never expire. Session tokens (issued via
	// IssueSession) always set it; the authn middleware denies a resolved key
	// once ExpiresAt is in the past, mirroring how RevokedAt is enforced.
	ExpiresAt *time.Time
	// LastUsedAt is when the key last authenticated a request, accurate to
	// about a minute (the middleware throttles the write). Nil for a key that
	// has not been used since last_used_at was added. Only List fills it:
	// Lookup leaves it nil so the auth path never depends on the column.
	LastUsedAt *time.Time
}

KeyRecord is a stored key as seen on the read paths. It deliberately carries NO hash or plaintext field: redaction is structural, so List can never leak a verifiable secret. TenantID == "" means the key is multi-tenant (NULL in the database).

type KeySpec

type KeySpec struct {
	Label         string
	TenantID      string
	CanManageKeys bool
}

KeySpec describes a key to be issued. TenantID == "" issues a multi-tenant key (stored as NULL tenant_id, callable against any tenant); a non-empty TenantID scopes the key to that single tenant.

type KeyStore

type KeyStore interface {
	// Issue mints a new key, persists its hash, and returns the plaintext once.
	Issue(ctx context.Context, spec KeySpec) (IssuedKey, error)
	// IssueSession mints a multi-tenant, manage-keys session token that
	// expires after ttl. It is the foundation for dashboard login: a session
	// is just an expiring KeyStore row, validated by the same middleware path
	// as any other API key.
	IssueSession(ctx context.Context, ttl time.Duration) (IssuedKey, error)
	// Lookup resolves a key by its sha256 hex hash with NO tenant scoping.
	// Revoked keys still return (found=true): revocation is enforced by the
	// middleware, not by hiding the row.
	Lookup(ctx context.Context, keyHash string) (rec KeyRecord, found bool, err error)
	// List returns every key as a redacted KeyRecord (no hash/plaintext).
	List(ctx context.Context) ([]KeyRecord, error)
	// Revoke stamps revoked_at on the key. The row stays visible to Lookup.
	// Revoking an already revoked key succeeds and keeps the first revoked_at;
	// an unknown id returns ErrKeyNotFound.
	Revoke(ctx context.Context, id string) error
}

KeyStore is the persistence port for hosted-fabriq API keys. It operates on the instance-global fabriq_api_key table (created by migration 0027), which is deliberately NOT under RLS: keys are resolved by hash before any tenant context exists, mirroring the outbox's tenant-less rationale.

func NewKeyStore

func NewKeyStore(gdb *grove.DB) KeyStore

NewKeyStore builds a KeyStore over a grove.DB. The grove MUST be backed by the pg driver (grove + pgdriver), which is fabriq's only supported backend; this mirrors postgres.OpenWithGrove's driver assertion.

type KeyToucher added in v1.9.0

type KeyToucher interface {
	// Touch records at as the key's last use. An unknown id is not an error.
	Touch(ctx context.Context, id string, at time.Time) error
}

KeyToucher is an optional KeyStore capability: a store that implements it gets the id of every key that authenticates a request, so it can record the last use. It is type-asserted by the authn middleware rather than added to KeyStore, so existing KeyStore implementations keep compiling and a store without it simply never reports a last use.

type Option

type Option func(*config)

Option configures the adminapi extension.

func WithAdminLogin

func WithAdminLogin(username, password string) Option

WithAdminLogin enables the dashboard-login surface (POST {BasePath}/login + /logout): username is compared verbatim at login time and password is bcrypt-hashed immediately, at option-application time — the plaintext password is never retained. WithAdminLogin REQUIRES WithAuth: login mints a session token via KeyStore.IssueSession, so Extension.Start fails fast with an error if AdminLoginUser is set but no KeyStore was configured.

func WithAnalyticsAdmin added in v0.0.6

func WithAnalyticsAdmin() Option

WithAnalyticsAdmin enables the cross-tenant analytics sink endpoints — synchronous backfill (single-tenant or fleet) plus a status check (the HTTP twin of `fabriq analytics backfill`). Leave OFF unless the host guards the admin API: backfill replays tenant snapshots into the analytics sink and is an instance-global operation.

func WithAnalyticsRead added in v0.0.6

func WithAnalyticsRead() Option

WithAnalyticsRead enables the read-only analytics status/freshness endpoint (GET /analytics/status) without the mutating operations — a least-privilege gate for a freshness dashboard. WithAnalyticsAdmin implies this.

func WithAuth

func WithAuth(store KeyStore) Option

WithAuth sets the instance-global API key store, enabling the /admin/keys issue/list/revoke management routes (registered only when the store is non-nil). It does NOT install the request-verifying middleware — that is a separate, explicit step: attach authMiddleware(store, basePath) via WithRouteOptions to actually gate admin requests on a valid key.

func WithAuthDisabled

func WithAuthDisabled() Option

WithAuthDisabled turns OFF secure-by-default auth: no KeyStore is auto-provisioned and the admin API is served without authentication. Use for trusted internal deployments or tests that intentionally run keyless.

func WithAuthRequired added in v1.9.0

func WithAuthRequired() Option

WithAuthRequired makes auth fail closed for a hosted server. Register returns an error when no KeyStore can be provisioned (no WithAuth and no reachable Postgres DB) or when WithAuthDisabled is also set, instead of serving the admin API unauthenticated. Start then requires FABRIQ_ADMIN_KEY to hold a well-formed key (fq_ followed by at least 32 letters or digits) and never generates and logs a bootstrap key.

WithAuthRequired alone does not turn off the default admin/admin dashboard login. A hosted server passes WithoutDefaultLogin as well.

func WithAuthorizer added in v0.0.6

func WithAuthorizer(a Authorizer) Option

WithAuthorizer overrides the default flag-based capability gating with a host-supplied Authorizer — the seam for per-user RBAC (e.g. bridging warden). fabriq itself imports no authz system; the host wires the adapter.

func WithBasePath

func WithBasePath(p string) Option

WithBasePath sets the admin API base path (default "/admin").

func WithConnectionsRead added in v0.0.6

func WithConnectionsRead() Option

WithConnectionsRead enables the connection/topology info endpoints — GET /connections (the tier's stores + cluster topology) and GET /tenants/:id/connection (a tenant's dedicated database). All connection info is REDACTED server-side: passwords and other secrets are never serialized. It is a read-only gate, deliberately separate from the mutating WithTenantsAdmin so a topology viewer can be granted without provisioning rights; leave OFF unless the host guards the admin API.

func WithEmbedder

func WithEmbedder(e agent.Embedder) Option

WithEmbedder supplies the embedding model used by the text-mode vector search endpoint (POST {BasePath}/search/vector with a {query} body). Fabriq stays model-agnostic: the host provides the implementation (Anthropic, OpenAI, a local model). When unset, text-mode vector search returns 501 and callers must use the similar-to-entity mode ({id} body) instead.

func WithOperatorOnlyInstanceOps added in v1.9.0

func WithOperatorOnlyInstanceOps() Option

WithOperatorOnlyInstanceOps limits the admin routes that reach past one tenant to operator keys, the ones with CanManageKeys. With it set, these answer 403 for any other key:

POST   {BasePath}/query                        raw SQL (query.raw)
DELETE {BasePath}/schema/:type                 drop a dynamic type
DELETE {BasePath}/schema/:type/fields/:column  drop a field
POST   {BasePath}/schema/:type/rename-field    rename a field

Raw SQL runs as the serving role, which can read instance tables that RLS does not fence (API keys, the tenant registry), and a dynamic type's table is shared by every tenant, so dropping or renaming it touches them all. Defining a type and adding fields stay open to any key. A request with no resolved key (auth off) is refused too, so the gate fails closed. A hosted server (fabriq serve) sets this; library defaults leave it off.

func WithRouteOptions

func WithRouteOptions(opts ...forge.RouteOption) Option

WithRouteOptions forwards forge route options (auth middleware, OpenAPI decorators) to all admin routes — the extension stays auth-agnostic.

func WithSchemaAdmin

func WithSchemaAdmin() Option

WithSchemaAdmin enables the privileged schema-ops endpoints — running/rolling back static migrations and executing ad-hoc DDL. These are instance-global, schema-owner operations; leave OFF unless the host guards the admin API.

func WithTenantRegistry added in v1.9.0

func WithTenantRegistry(r TenantRegistry) Option

WithTenantRegistry sets the static-mode tenant registry that serves POST/GET /tenants when the fabric runs without a catalog. With a registry set, POST /keys also refuses a tenantId that is not registered (404).

func WithTenantsAdmin added in v0.0.6

func WithTenantsAdmin() Option

WithTenantsAdmin enables the tenant-management endpoints — list/get plus the suspend/resume lifecycle (the HTTP twin of the `fabriq tenant` CLI). These are instance-global operations; leave OFF unless the host guards the admin API. Without a catalog, Register also provisions the static tenant registry (POST/GET /tenants), which makes POST /keys check tenantId against it.

func WithWritePolicy

func WithWritePolicy(p agent.WritePolicy) Option

WithWritePolicy sets the agent-toolkit write allowlist backing the guarded write endpoint (POST {BasePath}/agent/remember). Deny-by-default: without this option Remember permits no writes. Example: allow product creates/updates via agent.WritePolicy{Allow: map[string][]command.Op{"product": {command.OpCreate, command.OpUpdate}}}.

func WithoutDefaultLogin added in v1.9.0

func WithoutDefaultLogin() Option

WithoutDefaultLogin stops secure-by-default auth from enabling the admin/admin dashboard login. Login stays off unless the host sets WithAdminLogin.

type TenantRecord added in v1.9.0

type TenantRecord struct {
	ID        string
	Label     string
	CreatedAt time.Time
}

TenantRecord is one row of the static-mode tenant registry.

type TenantRegistry added in v1.9.0

type TenantRegistry interface {
	// Create records the tenant. An existing id returns the stored record and
	// created == false, with its label and created_at untouched.
	Create(ctx context.Context, id, label string) (rec TenantRecord, created bool, err error)
	// Get returns the tenant, or found == false when the id is unknown.
	Get(ctx context.Context, id string) (rec TenantRecord, found bool, err error)
	// List returns every tenant ordered by id.
	List(ctx context.Context) ([]TenantRecord, error)
}

TenantRegistry is the persistence port for static-mode tenants. In row-level security mode a tenant is only a string on each row, so the registry is the one place that records a tenant exists. It operates on the instance-global fabriq_tenant table (created by migration 0034), which is deliberately NOT under RLS. Catalog mode has its own directory and does not use it.

func NewTenantRegistry added in v1.9.0

func NewTenantRegistry(gdb *grove.DB) TenantRegistry

NewTenantRegistry builds a TenantRegistry over a grove.DB. The grove MUST be backed by the pg driver, like NewKeyStore.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL