approvals

package
v0.0.0-...-e6b51b8 Latest Latest
Warning

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

Go to latest
Published: Mar 27, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

Package approvals provides the data model and handlers for the approvals service.

Index

Constants

This section is empty.

Variables

View Source
var ErrApprovalRequestNotPendingOrExpired = errors.New("approval request not pending or expired")

Functions

func BuildApprovalRequestedCloudEvent

func BuildApprovalRequestedCloudEvent(n NotificationOutbox, source, summary string) ([]byte, error)

func ParseSecretRefMap

func ParseSecretRefMap(raw string) map[string]string

func SafeTransport

func SafeTransport() *http.Transport

SafeTransport returns an http.Transport that blocks connections to private, loopback, and link-local IP addresses (MED-02: DNS rebinding defense).

func SignBodyHMACSHA256

func SignBodyHMACSHA256(rawBody []byte, secret string) string

func ValidateWebhookURL

func ValidateWebhookURL(rawURL string) error

func VerifySlackRequest

func VerifySlackRequest(rawBody []byte, signatureHeader, timestampHeader, secret string, now time.Time) bool

Types

type ApprovalGrant

type ApprovalGrant struct {
	ID        string        `json:"id"`
	RequestID string        `json:"request_id"`
	TenantID  string        `json:"tenant_id"`
	Approver  string        `json:"approver"`
	Scope     ApprovalScope `json:"scope"`
	MaxUses   int           `json:"max_uses"`
	UsesLeft  int           `json:"uses_left"`
	ExpiresAt time.Time     `json:"expires_at"`
	GrantedAt time.Time     `json:"granted_at"`
}

type ApprovalRequest

type ApprovalRequest struct {
	ID         string    `json:"id"`
	EventID    string    `json:"event_id"`
	TenantID   string    `json:"tenant_id"`
	AgentID    string    `json:"agent_id"`
	UserID     string    `json:"user_id,omitempty"`
	UserName   string    `json:"user_name,omitempty"`
	UserEmail  string    `json:"user_email,omitempty"`
	SessionID  string    `json:"session_id,omitempty"`
	TraceID    string    `json:"trace_id,omitempty"`
	Tool       string    `json:"tool"`
	Action     string    `json:"action"`
	Resource   string    `json:"resource,omitempty"`
	RiskScore  int       `json:"risk_score"`
	Reason     string    `json:"reason"`
	DenyReason string    `json:"deny_reason,omitempty"`
	Status     string    `json:"status"` // "pending", "approved", "denied", "expired"
	CreatedAt  time.Time `json:"created_at"`
	ExpiresAt  time.Time `json:"expires_at"`
}

type ApprovalScope

type ApprovalScope struct {
	Tool            string `json:"tool"`             // exact or "*"
	Action          string `json:"action"`           // exact or "*"
	ResourcePattern string `json:"resource_pattern"` // glob pattern
	TenantID        string `json:"tenant_id"`
	AgentID         string `json:"agent_id,omitempty"` // optional restriction
}

type ApproverAuthorizer

type ApproverAuthorizer struct {
	// contains filtered or unexported fields
}

func NewApproverAuthorizer

func NewApproverAuthorizer(lookup ApproverLookup, emailAllowlist, slackAllowlist string, source string) *ApproverAuthorizer

NewApproverAuthorizer gates authorization by DB (users+user_roles) and optionally by env allowlists. env allowlists are intended as a dev bootstrap fallback.

func (*ApproverAuthorizer) AllowEmail

func (a *ApproverAuthorizer) AllowEmail(ctx context.Context, tenantID, email string) bool

AllowEmail checks whether the given email identity is an approver for the tenant. DB is the primary source of truth; env allowlists are optional depending on allowlist source.

func (*ApproverAuthorizer) AllowSlack

func (a *ApproverAuthorizer) AllowSlack(ctx context.Context, tenantID, slackUserID string) bool

func (*ApproverAuthorizer) ResolveSlackApprover

func (a *ApproverAuthorizer) ResolveSlackApprover(ctx context.Context, tenantID, slackUserID string) (email string, ok bool)

ResolveSlackApprover returns the console user email associated with the given Slack user id, but only if that user exists (slack_user_id) and is an approver for the tenant.

type ApproverLookup

type ApproverLookup interface {
	FindUserByEmail(context.Context, string) (*ConsoleUserIdentity, error)
	FindUserBySlackUserID(context.Context, string) (*ConsoleUserIdentity, error)
	IsApproverUserForTenant(context.Context, string, string) (bool, error)
}

type ConsoleUserIdentity

type ConsoleUserIdentity struct {
	ID    string
	Email string
}

ConsoleUserIdentity is a minimal identity mapping from console tables used for approver authorization.

type CreateApprovalInput

type CreateApprovalInput struct {
	EventID         string               `json:"event_id"`
	TenantID        string               `json:"tenant_id"`
	AgentID         string               `json:"agent_id"`
	Tool            string               `json:"tool"`
	Action          string               `json:"action"`
	Resource        string               `json:"resource,omitempty"`
	RiskScore       int                  `json:"risk_score"`
	RiskFactors     []string             `json:"risk_factors,omitempty"`
	Reason          string               `json:"reason"`
	TraceID         string               `json:"trace_id,omitempty"`
	ApproverGroup   string               `json:"approver_group,omitempty"`
	Notify          []types.PolicyNotify `json:"notify,omitempty"`
	ApprovalBaseURL string               `json:"approval_base_url,omitempty"`
}

type DenyInput

type DenyInput struct {
	Approver string `json:"approver"`
	Reason   string `json:"reason"`
}

type Dispatcher

type Dispatcher struct {
	SkipWebhookValidation bool // testing only — disables SSRF URL checks
	// contains filtered or unexported fields
}

func NewDispatcher

func NewDispatcher(store notificationStore, source string, secrets map[string]string, slackURL, internalToken string) *Dispatcher

func (*Dispatcher) DispatchOnce

func (d *Dispatcher) DispatchOnce(ctx context.Context) error

type GrantInput

type GrantInput struct {
	Approver        string `json:"approver"`
	MaxUses         int    `json:"max_uses"`
	ExpiresInSec    int    `json:"expires_in_sec"` // seconds from now
	ResourcePattern string `json:"resource_pattern,omitempty"`
}

type Handlers

type Handlers struct {
	// contains filtered or unexported fields
}

Handlers groups the HTTP handlers for the approvals service.

func NewHandlers

func NewHandlers(store handlersStore, authorizer *ApproverAuthorizer, slackSigningSecret string) *Handlers

NewHandlers creates handlers backed by the given store.

func (*Handlers) ApproveRequest

func (h *Handlers) ApproveRequest(w http.ResponseWriter, r *http.Request)

ApproveRequest handles POST /v1/approvals/requests/{id}/approve

func (*Handlers) CreateRequest

func (h *Handlers) CreateRequest(w http.ResponseWriter, r *http.Request)

CreateRequest handles POST /v1/approvals/requests

func (*Handlers) DenyRequest

func (h *Handlers) DenyRequest(w http.ResponseWriter, r *http.Request)

DenyRequest handles POST /v1/approvals/requests/{id}/deny

func (*Handlers) GetRequest

func (h *Handlers) GetRequest(w http.ResponseWriter, r *http.Request)

GetRequest handles GET /v1/approvals/requests/{id}

func (*Handlers) ListPending

func (h *Handlers) ListPending(w http.ResponseWriter, r *http.Request)

ListPending handles GET /v1/approvals/pending?tenant_id=...&limit=...&offset=...

func (*Handlers) RegisterRoutes

func (h *Handlers) RegisterRoutes(r chi.Router)

RegisterRoutes mounts the approval routes on r. These routes are internal-only (behind internalAuthMiddleware). Tenant isolation is enforced at the gateway layer; the approval service trusts tenant_id values from authenticated internal callers.

func (*Handlers) SlackInteractions

func (h *Handlers) SlackInteractions(w http.ResponseWriter, r *http.Request)

SlackInteractions handles POST /v1/integrations/slack/interactions.

type LLMSummaryProvider

type LLMSummaryProvider struct {
	// contains filtered or unexported fields
}

LLMSummaryProvider calls the llm-summarizer service over HTTP and falls back to the template provider on any error.

func NewLLMSummaryProvider

func NewLLMSummaryProvider(llmURL string) *LLMSummaryProvider

func (*LLMSummaryProvider) Summarize

func (p *LLMSummaryProvider) Summarize(ctx context.Context, input SummaryInput) (SummaryOutput, error)

type NotificationOutbox

type NotificationOutbox struct {
	ID                string
	ApprovalRequestID string
	TenantID          string
	EventID           string
	TraceID           string
	Tool              string
	Action            string
	Resource          string
	RiskScore         int
	RiskFactors       []string
	Reason            string
	ApprovalURL       string
	ApproverGroup     string
	NotifyKind        string
	NotifyURL         string
	SecretRef         string
	SlackChannel      string
	Attempts          int
	Status            string
	NextAttemptAt     time.Time
	CreatedAt         time.Time
}

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store manages approval requests and grants in Postgres.

func NewStore

func NewStore(pool *pgxpool.Pool) *Store

NewStore creates a new approvals store.

func (*Store) ClaimDueNotifications

func (s *Store) ClaimDueNotifications(ctx context.Context, limit int) ([]NotificationOutbox, error)

ClaimDueNotifications claims pending due rows for delivery using row-level locking so concurrent workers cannot deliver the same ID twice.

func (*Store) CreateRequest

func (s *Store) CreateRequest(ctx context.Context, in CreateApprovalInput) (*ApprovalRequest, error)

CreateRequest inserts a new pending approval request.

func (*Store) DenyRequest

func (s *Store) DenyRequest(ctx context.Context, requestID string, in DenyInput) error

DenyRequest marks a pending request as denied. The original reason is preserved; deny_reason stores the denier's rationale.

func (*Store) ExpirePendingRequests

func (s *Store) ExpirePendingRequests(ctx context.Context) (int64, error)

ExpirePendingRequests transitions stale pending requests to 'expired' (MED-07).

func (*Store) FindAndConsumeGrant

func (s *Store) FindAndConsumeGrant(ctx context.Context, tenantID, agentID, tool, action, resource string) (*ApprovalGrant, error)

FindAndConsumeGrant finds a valid grant matching the given scope and atomically decrements its usage. Iterates through all candidates (not just LIMIT 1) to ensure resource-pattern mismatches don't hide valid grants.

func (*Store) FindUserByEmail

func (s *Store) FindUserByEmail(ctx context.Context, email string) (*ConsoleUserIdentity, error)

FindUserByEmail returns a console user identity by email (case-insensitive).

func (*Store) FindUserBySlackUserID

func (s *Store) FindUserBySlackUserID(ctx context.Context, slackUserID string) (*ConsoleUserIdentity, error)

FindUserBySlackUserID returns a console user identity by slack user id.

func (*Store) GetRequest

func (s *Store) GetRequest(ctx context.Context, id string) (*ApprovalRequest, error)

GetRequest fetches a single approval request.

func (*Store) GrantRequest

func (s *Store) GrantRequest(ctx context.Context, requestID string, in GrantInput) (*ApprovalGrant, error)

GrantRequest approves a pending request, creating a grant. The status check is performed inside the transaction to eliminate TOCTOU races.

func (*Store) IsApproverUserForTenant

func (s *Store) IsApproverUserForTenant(ctx context.Context, tenantID, userID string) (bool, error)

IsApproverUserForTenant returns true if the given user has role='approver' for the given tenant.

func (*Store) ListPending

func (s *Store) ListPending(ctx context.Context, tenantID string, limit, offset int) ([]ApprovalRequest, error)

ListPending returns pending requests for a tenant (paginated). When tenantID is empty, all pending requests across tenants are returned (used by platform admins).

func (*Store) MarkNotificationFailed

func (s *Store) MarkNotificationFailed(ctx context.Context, id string, lastErr string) error

MarkNotificationFailed marks an outbox row terminally failed.

func (*Store) MarkNotificationRetry

func (s *Store) MarkNotificationRetry(ctx context.Context, id string, attempts int, nextAttemptAt time.Time, lastErr string) error

MarkNotificationRetry schedules another delivery attempt with backoff.

func (*Store) MarkNotificationSent

func (s *Store) MarkNotificationSent(ctx context.Context, id string) error

MarkNotificationSent marks an outbox record as delivered.

type Summarizer

type Summarizer interface {
	Summarize(NotificationOutbox) string
}

Summarizer builds human-friendly notification summaries from sanitized fields.

type SummaryInput

type SummaryInput struct {
	Tool        string   `json:"tool"`
	Action      string   `json:"action"`
	Resource    string   `json:"resource"`
	RiskScore   int      `json:"risk_score"`
	RiskFactors []string `json:"risk_factors"`
	Reason      string   `json:"reason"`
	TenantID    string   `json:"tenant_id"`
	AgentID     string   `json:"agent_id"`
}

func SummaryInputFromOutbox

func SummaryInputFromOutbox(n NotificationOutbox) SummaryInput

SummaryInputFromOutbox converts a NotificationOutbox into sanitized SummaryInput.

type SummaryOutput

type SummaryOutput struct {
	SummaryText string `json:"summary_text"`
	ModelID     string `json:"model_id,omitempty"`
	LatencyMS   int64  `json:"latency_ms,omitempty"`
	FromCache   bool   `json:"from_cache,omitempty"`
}

type SummaryProvider

type SummaryProvider interface {
	Summarize(ctx context.Context, input SummaryInput) (SummaryOutput, error)
}

SummaryProvider produces human-readable summaries for approval notifications. Implementations range from deterministic templates to LLM-backed summarizers.

type TemplateSummarizer

type TemplateSummarizer struct{}

TemplateSummarizer is deterministic and does not use model inference.

func (TemplateSummarizer) Summarize

type TemplateSummaryProvider

type TemplateSummaryProvider struct{}

TemplateSummaryProvider is a deterministic, zero-dependency summary provider.

func (TemplateSummaryProvider) Summarize

Jump to

Keyboard shortcuts

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