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
- Variables
- func Compile(req *auditv1.SearchRequest) (index.Query, error)
- func FileKey(id, format string) string
- func NewHandler(s *Service, a auth.Authenticator, opts ...connect.HandlerOption) (string, http.Handler)
- type Exporter
- type Handler
- func (h *Handler) Access(ctx context.Context, req *connect.Request[auditv1.AccessRequest]) (*connect.Response[auditv1.AccessResponse], error)
- func (h *Handler) Export(ctx context.Context, req *connect.Request[auditv1.ExportRequest]) (*connect.Response[auditv1.ExportResponse], error)
- func (h *Handler) Facets(ctx context.Context, req *connect.Request[auditv1.FacetsRequest]) (*connect.Response[auditv1.FacetsResponse], error)
- func (h *Handler) Get(ctx context.Context, req *connect.Request[auditv1.GetRequest]) (*connect.Response[auditv1.GetResponse], error)
- func (h *Handler) GetExport(ctx context.Context, req *connect.Request[auditv1.GetExportRequest]) (*connect.Response[auditv1.GetExportResponse], error)
- func (h *Handler) Resolve(ctx context.Context, req *connect.Request[auditv1.ResolveRequest]) (*connect.Response[auditv1.ResolveResponse], error)
- func (h *Handler) Search(ctx context.Context, req *connect.Request[auditv1.SearchRequest]) (*connect.Response[auditv1.SearchResponse], error)
- type Job
- type ProfileAccess
- type Service
- func (s *Service) Access(ctx context.Context, p auth.Principal) ([]ProfileAccess, error)
- func (s *Service) Close() error
- func (s *Service) Cursors(req *auditv1.SearchRequest, g auth.Grant, page index.Page) (*auditv1.Cursors, error)
- func (s *Service) Export(ctx context.Context, p auth.Principal, req *auditv1.ExportRequest) (Job, error)
- func (s *Service) Facets(ctx context.Context, p auth.Principal, req *auditv1.FacetsRequest) ([]index.Facet, auth.Grant, error)
- func (s *Service) Get(ctx context.Context, p auth.Principal, req *auditv1.GetRequest) (index.Row, index.Provenance, auth.Grant, error)
- func (s *Service) GetExport(ctx context.Context, p auth.Principal, id string) (Job, string, error)
- func (s *Service) Resolve(ctx context.Context, p auth.Principal, req *auditv1.ResolveRequest) (string, error)
- func (s *Service) Search(ctx context.Context, p auth.Principal, req *auditv1.SearchRequest) (index.Page, auth.Grant, error)
Constants ¶
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 ¶
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.
var ErrCursorMismatch = errors.New("query: this cursor belongs to a different query")
ErrCursorMismatch is returned for a cursor from another query.
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 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 ¶
func (h *Handler) Access( ctx context.Context, req *connect.Request[auditv1.AccessRequest], ) (*connect.Response[auditv1.AccessResponse], error)
Access implements the service.
func (*Handler) Export ¶
func (h *Handler) Export( ctx context.Context, req *connect.Request[auditv1.ExportRequest], ) (*connect.Response[auditv1.ExportResponse], error)
Export implements the service.
func (*Handler) Facets ¶
func (h *Handler) Facets( ctx context.Context, req *connect.Request[auditv1.FacetsRequest], ) (*connect.Response[auditv1.FacetsResponse], error)
Facets implements the service.
func (*Handler) Get ¶
func (h *Handler) Get( ctx context.Context, req *connect.Request[auditv1.GetRequest], ) (*connect.Response[auditv1.GetResponse], error)
Get implements the service.
func (*Handler) GetExport ¶
func (h *Handler) GetExport( ctx context.Context, req *connect.Request[auditv1.GetExportRequest], ) (*connect.Response[auditv1.GetExportResponse], error)
GetExport 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.
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 (*Service) Access ¶
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) 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 ¶
func (s *Service) Get( ctx context.Context, p auth.Principal, req *auditv1.GetRequest, ) (index.Row, index.Provenance, auth.Grant, error)
Get answers one record, and refuses one the grant's tenants do not cover.
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.