query

package
v0.16.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 29 Imported by: 0

Documentation

Overview

Package query is the read service: it turns a request into a closed query, narrows it to what the caller was granted, asks a searcher, and records that the read happened.

It is internal because the contract is the API, not this Go package. What a third party implements is the Searcher below it and the Authenticator and Authorizer above it, both of which are public.

Index

Constants

View Source
const ExportPrefix = "export"

ExportPrefix is where exports live.

Outside every profile's prefix, deliberately. An export is a copy of records made to be taken away, and it must not be mistaken for the archive: it is not under an object lock, no seal covers it, and it expires. A reader who found one under a profile prefix would have no way to tell it from a record.

Variables

View Source
var (
	// ErrMalformed is a request this service cannot read. The caller fixes it.
	ErrMalformed = errors.New("query: malformed request")
	// ErrTooMuch is a request that asks for more than a limit allows.
	ErrTooMuch = errors.New("query: more than this deployment allows")
	// ErrNotOffered is something this deployment does not do.
	ErrNotOffered = errors.New("query: not offered by this deployment")
	// ErrNotFound is a record or a job that is not there — or that the caller
	// may not know is.
	ErrNotFound = errors.New("query: not found")
)

The kinds of failure a caller can tell apart. They exist so that the wire mapping is a fact about the error rather than a guess from its text.

View Source
var ErrCursorMismatch = errors.New("query: this cursor belongs to a different query")

ErrCursorMismatch is returned for a cursor from another query.

View Source
var ErrErased = errors.New("query: the key behind this pseudonym has been destroyed; it can no longer be resolved")

ErrErased is a pseudonym whose tenant key has been destroyed: the way back is gone on purpose, and saying so is the answer rather than a failure.

Functions

func Compile

func Compile(req *auditv1.SearchRequest) (index.Query, error)

Compile turns a request into a closed query.

Every predicate is named by the caller from a fixed set; nothing here parses an expression. That is what makes a grant expressible as one more term rather than as a rewrite of something arbitrary.

func FileKey

func FileKey(id, format string) string

FileKey is where the export itself is written.

func NewHandler

func NewHandler(s *Service, a auth.Authenticator, opts ...connect.HandlerOption) (string, http.Handler)

NewHandler returns the path and handler to mount.

Types

type Exporter

type Exporter struct {
	// Store is where exports are written. It may be the archive's bucket under
	// the export prefix, or a bucket of its own; it must not be a prefix any
	// profile writes to.
	Store store.Store
	// Presigner makes the link. Without one an export can be produced and not
	// collected, so the service refuses rather than filling a bucket with
	// files nobody can reach.
	Presigner store.Presigner
	// Expiry is how long an export is kept. It has a short default and no
	// unlimited setting: an export is an unlocked copy of audit records, and
	// one that outlives the question it answered is a second archive that
	// nobody is managing. Default 7 days.
	Expiry time.Duration
	// MaxRecords bounds one export. Beyond it the job fails and says to narrow
	// the filter, rather than reading a whole profile into memory because a
	// grant happened to allow it. Default 100000.
	MaxRecords int
	// LinkValid is how long a link works. Default one hour: a URL to audit
	// records that outlives the conversation it was shared in is a copy of the
	// trail nobody is tracking.
	LinkValid time.Duration
	Now       func() time.Time
}

Exporter writes exports and hands out links to them.

type Handler

type Handler struct {
	auditv1connect.UnimplementedQueryServiceHandler
	Service *Service
	// Authenticator establishes who is asking. There is no default: a handler
	// that answered without one would answer anybody.
	Authenticator auth.Authenticator
}

Handler serves the query service over Connect.

func (*Handler) Access

Access implements the service.

func (*Handler) Export

Export implements the service.

func (*Handler) Facets

Facets implements the service.

func (*Handler) Get

Get implements the service.

func (*Handler) GetExport

GetExport implements the service.

func (*Handler) Resolve

Resolve implements the service.

func (*Handler) Search

Search implements the service.

type Job

type Job struct {
	ID        string     `json:"id"`
	Profile   string     `json:"profile"`
	Format    string     `json:"format"`
	By        string     `json:"by"`
	Rule      string     `json:"rule"`
	AskedAt   time.Time  `json:"asked_at"`
	Records   int        `json:"records,omitempty"`
	DoneAt    *time.Time `json:"done_at,omitempty"`
	Failed    string     `json:"failed,omitempty"`
	ExpiresAt time.Time  `json:"expires_at"`
}

Job is what was asked for and what became of it.

func (Job) State

func (j Job) State() auditv1.ExportState

State is where a job has got to.

type ProfileAccess

type ProfileAccess struct {
	Profile    string
	Operations []auth.Operation
	// Grant is the effective grant for search, or for the first operation
	// allowed when search is not: its tenants and window.
	Grant auth.Grant
}

ProfileAccess is what the caller may do on one profile.

type Service

type Service struct {
	Searcher index.Searcher
	// Authorizer decides what the caller may see. There is no default: a
	// service that answered without one would answer everything.
	Authorizer auth.Authorizer
	// Sink and Catalogue are where reads are recorded. Without them the service
	// runs and the reading of the trail leaves no trace, which a deployment
	// should have to choose rather than fall into.
	Sink      sink.Sink
	Catalogue *catalogue.Catalogue
	// Exporter writes exports, when a deployment offers them. Without one the
	// export operation is refused: a grant may name it, and nothing here will
	// produce a copy of records that the deployment did not configure a place
	// for.
	Exporter *Exporter
	// Identities, when given, is where Resolve finds the way back from a
	// pseudonym. Without it Resolve is not offered.
	Identities *identity.Map
	// Seals, when given, is the archive the seals are looked for in: a record
	// read from an object whose hour has been sealed says which seal covers it.
	Seals    store.Store
	Version  string
	Instance string
	// OnUnrecorded is called when a read happened and the trail does not say
	// so. A deployment alerts on it: the reading of an audit trail going
	// unrecorded is not a degraded service, it is the service failing at one of
	// the two things it is for.
	OnUnrecorded func(action string, err error)
	// contains filtered or unexported fields
}

Service answers queries, within a grant, and records that it did.

Reads of an audit trail are themselves auditable events: a trail that shows what everyone did except who looked at it is missing the half an investigation usually starts from. Every answer here produces a record naming the caller, what they asked and the rule that let them.

func New

func New(s *Service) (*Service, error)

New checks a service's parts and prepares its own emitter.

func (*Service) Access

func (s *Service) Access(ctx context.Context, p auth.Principal) ([]ProfileAccess, error)

Access says what the caller may read, profile by profile, from the same grants and the same rule (Effective) every other call is held to. It reads no record and so records nothing: what a person may see is not something they saw.

func (*Service) Close

func (s *Service) Close() error

Close drains what is pending.

func (*Service) Cursors

func (s *Service) Cursors(
	req *auditv1.SearchRequest, g auth.Grant, page index.Page,
) (*auditv1.Cursors, error)

Cursors renders a page's boundaries for the wire.

`next` is present even on the last page, because a tail keeps polling it and a record written afterwards has to come back through it. There is no `last`: counting what is behind a query is the expense keyset paging exists to avoid.

func (*Service) Export

func (s *Service) Export(
	ctx context.Context, p auth.Principal, req *auditv1.ExportRequest,
) (Job, error)

Export runs an export and records it.

It runs to completion before returning, which is the honest shape for what this can currently promise: a job handed to a goroutine would be lost by a restart, and a caller polling for a job no replica remembers is worse than waiting. The state machine in the contract is kept because an implementation that does defer the work will need it, and because the caller's code should not change when it does.

func (*Service) Facets

func (s *Service) Facets(
	ctx context.Context, p auth.Principal, req *auditv1.FacetsRequest,
) ([]index.Facet, auth.Grant, error)

Facets answers counts, narrowed to the grant.

func (*Service) Get

Get answers one record, and refuses one the grant's tenants do not cover.

func (*Service) GetExport

func (s *Service) GetExport(ctx context.Context, p auth.Principal, id string) (Job, string, error)

GetExport returns a job and, when it is ready, a link to it.

func (*Service) Resolve

func (s *Service) Resolve(
	ctx context.Context, p auth.Principal, req *auditv1.ResolveRequest,
) (string, error)

Resolve maps a pseudonym back to the identity behind it.

It is the one read that undoes the pseudonymisation, so it is fenced three ways. It needs the resolve operation on the profile, which no read grant implies and no group name grants (only an explicit rule naming a person). The tenant must be one the grant covers. And the resolution is recorded, and the record confirmed, before the identity is returned: if the trail cannot take it, nothing is resolved. The record names the pseudonym and the rule — never the identity it resolved to.

func (*Service) Search

Search answers a page, narrowed to the grant.

Jump to

Keyboard shortcuts

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