Skip to main content

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

source

var portalAssets

var portalAssets embed.FS

portalAssets are the portal's fonts and their licences, compiled in. See asset.

source

var portalTemplates

var portalTemplates = template.Must(template.ParseFS(portalFiles, "portal/*.html"))

source

const PortalPrefix

const PortalPrefix = "/portal"

PortalPrefix is where the portal lives on the manage role's listener.

source

const portalCookie

const portalCookie = "taisce_portal"

source

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.

source

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.

source

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
}

source

func NewPortal

func NewPortal(m *ManagementServer) *Portal

source

method Portal.Mount

func (p *Portal) Mount(mux *http.ServeMux)

Mount registers the portal's routes on a mux the manage role serves.

source

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
}

source

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.

source

type portalOutcome

type portalOutcome struct {
Done bool
Action string
Detail string
}

source

type portalScope

type portalScope struct {
Project string
Suspended bool
Stored *int64
Formed *int64
Behind int64
Parked int
}

source

type portalProject

type portalProject struct {
Name string
Refusals pg.RefusalSummary
Erasures []pg.ErasureReceipt
Parked pg.ParkedPage
Credentials []credential.Listed
}

source

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
}

source

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.

source

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.

source

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.

source

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.

source

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.

source

method Portal.render

func (p *Portal) render(w http.ResponseWriter, status int, page portalPage)

source

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.

source

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.

source

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.

source

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.

source

method Portal.take

func (p *Portal) take(id string) (*portalOutcome, string)

source

method Portal.forget

func (p *Portal) forget(w http.ResponseWriter, id string)

source

method Portal.loginForm

func (p *Portal) loginForm(w http.ResponseWriter, r *http.Request)

source

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.

source

method Portal.logout

func (p *Portal) logout(w http.ResponseWriter, r *http.Request)

source

method Portal.home

func (p *Portal) home(w http.ResponseWriter, r *http.Request, grant credential.Grant)

source

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.

source

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.

source

method Portal.project

func (p *Portal) project(w http.ResponseWriter, r *http.Request, grant credential.Grant)

source

method Portal.ledger

func (p *Portal) ledger(w http.ResponseWriter, r *http.Request, grant credential.Grant)

source

func max64

func max64(a, b int64) int64

source

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.

source

func itoa

func itoa(n int) string

source