Skip to Content
Custom ProvidersAuthorization

Authorization

This page covers building custom authorization providers. If you only need to configure authorization storage for a deployment, use Providers > Authorization.

Authorization providers are host-scoped policy engines and relationship stores. Gestalt uses them for dynamic subject authorization, built-in admin authorization APIs, and authorization model lifecycle.

Manifest

An authorization provider manifest declares kind: authorization:

kind: authorization source: github.com/your-org/authorization/example version: 0.0.1 displayName: Example Authorization description: Authorization provider with custom model and relationship storage. spec: configSchemaPath: ./authorization_config.json

Provider interface

Authorization provider authoring is currently exposed through the Go SDK.

The contract has three parts:

AreaMethods
Decision planeEvaluate, EvaluateMany, SearchResources, SearchSubjects, SearchActions, GetMetadata
Relationship control planeReadRelationships, WriteRelationships
Model lifecycleGetActiveModel, ListModels, WriteModel

The example below shows the shape of a minimal in-memory provider. The remaining methods follow the same request/response pattern.

Observability

Provider authors do not need to emit the baseline gestaltd authorization metrics. gestaltd wraps the configured provider interface and records gestaltd.authorization.provider.operation.* for every provider method. When the provider is used for provider-backed subject access, gestaltd also records gestaltd.authorization.provider.evaluate.* around the batched authorization checks it performs.

When OTLP tracing is enabled, preserve the context.Context passed into provider methods for downstream calls so provider work remains attached to the same trace tree.

package exampleauthz import ( "context" "fmt" gestalt "github.com/valon-technologies/gestalt/sdk/go" ) type Provider struct { modelID string } func New() *Provider { return &Provider{} } func (p *Provider) Configure(_ context.Context, _ string, config map[string]any) error { p.modelID, _ = config["modelId"].(string) return nil } func (p *Provider) Evaluate(_ context.Context, req *gestalt.AccessEvaluationRequest) (*gestalt.AccessDecision, error) { allowed := req.GetAction().GetName() == "read" return &gestalt.AccessDecision{ Allowed: allowed, ModelId: p.modelID, }, nil } func (p *Provider) EvaluateMany(ctx context.Context, req *gestalt.AccessEvaluationsRequest) (*gestalt.AccessEvaluationsResponse, error) { decisions := make([]*gestalt.AccessDecision, 0, len(req.GetRequests())) for _, item := range req.GetRequests() { decision, err := p.Evaluate(ctx, item) if err != nil { return nil, err } decisions = append(decisions, decision) } return &gestalt.AccessEvaluationsResponse{Decisions: decisions}, nil } func (p *Provider) SearchResources(context.Context, *gestalt.ResourceSearchRequest) (*gestalt.ResourceSearchResponse, error) { return &gestalt.ResourceSearchResponse{ModelId: p.modelID}, nil } func (p *Provider) SearchSubjects(context.Context, *gestalt.SubjectSearchRequest) (*gestalt.SubjectSearchResponse, error) { return &gestalt.SubjectSearchResponse{ModelId: p.modelID}, nil } func (p *Provider) SearchActions(context.Context, *gestalt.ActionSearchRequest) (*gestalt.ActionSearchResponse, error) { return &gestalt.ActionSearchResponse{ModelId: p.modelID}, nil } func (p *Provider) GetMetadata(context.Context) (*gestalt.AuthorizationMetadata, error) { return &gestalt.AuthorizationMetadata{ Capabilities: []string{"evaluate", "relationships", "models"}, ActiveModelId: p.modelID, }, nil } func (p *Provider) ReadRelationships(context.Context, *gestalt.ReadRelationshipsRequest) (*gestalt.ReadRelationshipsResponse, error) { return &gestalt.ReadRelationshipsResponse{ModelId: p.modelID}, nil } func (p *Provider) WriteRelationships(context.Context, *gestalt.WriteRelationshipsRequest) error { return nil } func (p *Provider) GetActiveModel(context.Context) (*gestalt.GetActiveModelResponse, error) { if p.modelID == "" { return &gestalt.GetActiveModelResponse{}, nil } return &gestalt.GetActiveModelResponse{ Model: &gestalt.AuthorizationModelRef{Id: p.modelID, Version: "v1"}, }, nil } func (p *Provider) ListModels(context.Context, *gestalt.ListModelsRequest) (*gestalt.ListModelsResponse, error) { if p.modelID == "" { return &gestalt.ListModelsResponse{}, nil } return &gestalt.ListModelsResponse{ Models: []*gestalt.AuthorizationModelRef{{Id: p.modelID, Version: "v1"}}, }, nil } func (p *Provider) WriteModel(_ context.Context, req *gestalt.WriteModelRequest) (*gestalt.AuthorizationModelRef, error) { if req.GetModel() == nil { return nil, fmt.Errorf("model is required") } p.modelID = "example-model" return &gestalt.AuthorizationModelRef{Id: p.modelID, Version: "v1"}, nil } func main() { if err := gestalt.ServeAuthorizationProvider(context.Background(), New()); err != nil { panic(err) } }

Config reference

To use a custom authorization provider in a deployment, reference it as a named entry in providers.authorization:

server: providers: indexeddb: main authorization: example providers: indexeddb: main: source: https://artifacts.example.com/indexeddb/relationaldb/v0.0.1-alpha.1/provider-release.yaml config: dsn: ${DATABASE_URL} authorization: example: source: ./authorization/manifest.yaml config: modelId: default

Use source: ./manifest.yaml during development. For production, publish a release and point source at the resulting provider-release.yaml metadata URL.