internal/api/portal.go
internal/api · 639 lines · 39 declarations · source
This file carries the package documentation, rendered on the package page.
Declarations
var portalFiles
var portalFiles embed.FS
var portalAssets
var portalAssets embed.FS
portalAssets are the portal's fonts and their licences, compiled in. See asset.
var portalTemplates
var portalTemplates = template.Must(template.ParseFS(portalFiles, "portal/*.html"))
const PortalPrefix
const PortalPrefix = "/portal"
PortalPrefix is where the portal lives on the manage role's listener.
const portalCookie
const portalCookie = "taisce_portal"
const portalSessionLife
const portalSessionLife = 12 * time.Hour
portalSessionLife bounds a browser session; the token behind it is resolved on every request anyway, so this is a ceiling on how long a forgotten browser stays signed in, not the check.
type Portal
type Portal struct {
m *ManagementServer
mu sync.Mutex
sessions map[string]portalSession
}
Portal serves the operator's view over a ManagementServer's stores.
type portalSession
type portalSession struct {
token string
expires time.Time
// guard is this session's synchroniser token, rendered into every form that acts and required
// back on every request that does.
//
// The cookie is already SameSite=Strict and the page's own policy is `form-action 'self'`, so a
// cross-site form post is refused twice before reaching here. This is the third, and it is worth
// having because the first two are promises made by the browser: an old one, a misconfigured
// proxy that strips the header, or a future relaxation of what "same site" means, and the page
// is acting on somebody else's request. A value the attacker cannot read does not depend on the
// browser behaving.
guard string
// outcome is the last action's result, waiting for the page that reports it.
outcome *portalOutcome
}
func NewPortal
func NewPortal(m *ManagementServer) *Portal
method Portal.Mount
func (p *Portal) Mount(mux *http.ServeMux)
Mount registers the portal's routes on a mux the manage role serves.
type portalPage
type portalPage struct {
Title string
Operator string
Login bool
Refused bool
Home bool
Scopes []portalScope
Project *portalProject
Ledger *portalLedger
// Guard is this session's synchroniser token, rendered into every form that acts.
Guard string
// Health is the instance's operational snapshot. Absent when the deployment did not wire a
// reader, which loses this panel and no other.
Health *portalHealth
// Activity is the ledger counted over the window the overview was asked for, and
// ActivityUnavailable says it could not be, so an empty panel is never read as an idle instance.
Activity *portalActivity
ActivityUnavailable bool
// Switch moves between projects, and is absent when the projects could not be listed.
Switch *portalSwitch
// Outcome is what the last action did, carried to the page that follows it. Held in the session
// rather than in the URL: an outcome in a query string is one a caller can put there, and a
// page that reports a revocation nobody performed is worse than one that reports nothing.
Outcome *portalOutcome
}
type portalHealth
type portalHealth struct {
Pending int64
PendingLimit int64
Parked int64
WorkerResponsive bool
FailedAttempts int64
Connections int64
MaxConnections int64
Headroom int64
// Tight is the judgement, made here rather than left to the reader: an operator scanning a page
// should not have to do arithmetic to notice they are nearly out of connections.
Tight bool
// PendingShare and ConnectionShare are the two meters, as whole percentages: a template cannot
// divide, and a bar is read faster than two numbers.
PendingShare, ConnectionShare int
}
portalOutcome is an action's result as the operator reads it.
Text this file wrote, never text from a store or an error. A message assembled from a database failure is how a table name, a constraint or a row's content reaches a browser, and the page an operator leaves open is the last place any of those should appear. portalHealth is what an operator watches when they are not looking for anything in particular.
Every number here is an aggregate the ledger or a catalogue view already holds. None of it is about a person: a backlog depth, whether a worker answered, and how close the deployment is to the server's connection ceiling — the last of which a deployment learned the hard way, by running out of sessions and losing writes.
type portalOutcome
type portalOutcome struct {
Done bool
Action string
Detail string
}
type portalScope
type portalScope struct {
Project string
Suspended bool
Stored *int64
Formed *int64
Behind int64
Parked int
}
type portalProject
type portalProject struct {
Name string
Refusals pg.RefusalSummary
Erasures []pg.ErasureReceipt
Parked pg.ParkedPage
Credentials []credential.Listed
}
type portalLedger
type portalLedger struct {
Verified bool
Detail string
Document string
// Entries are the ledger's own rows: who did what, to which project, how much of it, and
// whether it was allowed. No content by construction — the table holds none — so showing them
// discloses nothing a verdict does not, and answers the question a verdict cannot.
Entries []domain.AuditEntry
// Unavailable says the rows could not be read, so an empty table is never mistaken for an
// empty ledger.
Unavailable bool
// Project is the one project the rows are narrowed to, or empty for all of them.
Project string
}
const portalPolicy
const portalPolicy = "default-src 'none'; style-src 'unsafe-inline'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'"
portalPolicy is the pages' content security policy. Named so the test holds the exact string rather than a copy of it.
method Portal.asset
func (p *Portal) asset(w http.ResponseWriter, r *http.Request)
asset serves the portal's own fonts and their licences.
From the binary, not from a font service. An operator's console that fetched its typeface from somebody else's server would tell that server when, and from where, the console was opened, and would lose its type the day the deployment has no route out. Public, because the sign-in page needs them before anybody has signed in, and nothing here belongs to anybody: they are the files compiled into every copy of the binary.
Cached for a day and not forever: the names carry no version, so an upgrade that changed a font would otherwise be invisible to a browser that had seen the old one.
func share
func share(n, of int64) int
share is n as a whole percentage of of, for a meter.
Clamped at both ends: a backlog read a moment past its limit, or a connection count taken after the ceiling was lowered, would otherwise draw a bar out of its box, and a ceiling of zero would divide by it.
const portalLedgerRows
const portalLedgerRows = 100
portalLedgerRows bounds the panel. The ledger grows forever and a page does not; an operator reading further uses the management API, which pages properly.
method Portal.renderFor
func (p *Portal) renderFor(w http.ResponseWriter, r *http.Request, status int, page portalPage)
renderFor is render for a signed-in page: it attaches the session's guard so forms can act, and takes any outcome the last action left. Separate from render, which the sign-in page uses and which must never carry either.
method Portal.render
func (p *Portal) render(w http.ResponseWriter, status int, page portalPage)
method Portal.signed
func (p *Portal) signed(next func(http.ResponseWriter, *http.Request, credential.Grant)) http.HandlerFunc
signed resolves the browser session to an operator grant, or sends the browser to sign in with nothing shown. The token is resolved again each time: a revoked operator is signed out by the registry, not by a timer.
method Portal.acting
func (p *Portal) acting(next func(http.ResponseWriter, *http.Request, credential.Grant, string)) http.HandlerFunc
acting is `signed` for a request that changes something.
It adds the two things reading does not need. The session's synchroniser token must come back in the form, so a request the operator did not make from this page is refused before anything runs. And the handler is given the session id, because an action's outcome is rendered on the next page and the page needs the guard again.
Everything else is deliberately the same: the operator credential is re-resolved here as it is for a read, so a revoked operator cannot act with a live browser session any more than they can look.
method Portal.refuse
func (p *Portal) refuse(w http.ResponseWriter, r *http.Request, grant credential.Grant, detail string)
refuse renders the page the operator was on with a refusal, and records it.
A refused action is on the ledger for the same reason a refused operation is: the record answers "who tried", and an attempt that is turned away is exactly the kind somebody asks about later.
method Portal.remember
func (p *Portal) remember(id string, outcome portalOutcome)
remember holds an action's outcome until the next page renders it, then drops it.
method Portal.take
func (p *Portal) take(id string) (*portalOutcome, string)
method Portal.forget
func (p *Portal) forget(w http.ResponseWriter, id string)
method Portal.loginForm
func (p *Portal) loginForm(w http.ResponseWriter, r *http.Request)
method Portal.login
func (p *Portal) login(w http.ResponseWriter, r *http.Request)
login exchanges an operator token for a session. A refused token is answered like a stranger's: one page, no detail, and the refusal on the ledger through the surface's sampling budget.
method Portal.logout
func (p *Portal) logout(w http.ResponseWriter, r *http.Request)
method Portal.home
func (p *Portal) home(w http.ResponseWriter, r *http.Request, grant credential.Grant)
method Portal.health
func (p *Portal) health(r *http.Request) *portalHealth
health is the instance's operational snapshot for the page and the rail, or nil when the deployment wired no reader or the read failed — a monitoring read loses its panel, never the page.
method Portal.projectNames
func (p *Portal) projectNames(r *http.Request, grant credential.Grant) []string
projectNames lists the instance's projects for the switcher, on the ledger like any listing an operator's page makes. A failure loses the switcher and nothing else: it is a way to move between pages, not a page.
method Portal.project
func (p *Portal) project(w http.ResponseWriter, r *http.Request, grant credential.Grant)
method Portal.ledger
func (p *Portal) ledger(w http.ResponseWriter, r *http.Request, grant credential.Grant)
func max64
func max64(a, b int64) int64
func ledgerVerdict
func ledgerVerdict(v domain.AuditVerification) (bool, string)
ledgerVerdict says in a sentence what the verification found, because a page that shows only a boolean tells nobody what happened.
func itoa
func itoa(n int) string