docs: publish Preview 19 dogfood evidence

Export the reviewed allowlisted snapshot from private source commit 05928cebd01b586cf9e9d4b8c8537a7605a6068c. This records the exact candidate, bounded capacity result, stateful migration scratch requirement, authenticated batch identity proof, and immediate live acceptance evidence.

AI-Assisted: OpenAI Codex
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
2026-08-18 21:47:08 -04:00
commit 92a66db3df
201 changed files with 38227 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"errors"
"fmt"
"net/http"
"net/url"
"time"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/query"
"gamertan.com/observatory/internal/site"
"gamertan.com/web/access"
"gamertan.com/web/auth"
"gamertan.com/web/authhttp"
)
const defaultExploreQuery = "logs | window 1h | limit 50"
func (s *Server) explorePage(w http.ResponseWriter, r *http.Request) {
view, _, ok := s.exploreView(w, r, defaultExploreQuery)
if !ok {
return
}
s.renderHTML(w, r, http.StatusOK, site.Explore(view))
}
func (s *Server) exploreForm(w http.ResponseWriter, r *http.Request) {
values, err := readForm(w, r, 20<<10, "csrf_token", "query")
queryText := defaultExploreQuery
if err == nil {
queryText = values.Get("query")
}
view, token, ok := s.exploreView(w, r, queryText)
if !ok {
return
}
csrfOK := err == nil && authhttp.VerifyCSRF(token, "query:execute", values.Get("csrf_token"))
if !tokenBoundFormRequest(r, s.options.PublicOrigin, csrfOK) {
view.ErrorMessage = "This query form expired or could not be verified. Please try again."
s.renderHTML(w, r, http.StatusForbidden, site.Explore(view))
return
}
if err != nil {
view.ErrorMessage = "The query form was not accepted."
s.renderHTML(w, r, http.StatusBadRequest, site.Explore(view))
return
}
ast, err := query.Parse(queryText, s.options.MaxQueryRows)
if err != nil {
view.ErrorMessage = "The query could not be parsed. Check its stages, values, window, and limit."
s.renderHTML(w, r, http.StatusUnprocessableEntity, site.Explore(view))
return
}
principal, _ := auth.PrincipalFromContext(r.Context())
scope := access.Scope{OrganizationID: view.Organization.ID}
sensitive, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionTelemetryReadSensitive)
if err != nil {
view.ErrorMessage = "Authorization is temporarily unavailable."
s.renderHTML(w, r, http.StatusServiceUnavailable, site.Explore(view))
return
}
result, err := s.store.Query(r.Context(), ast, query.Scope{OrganizationID: view.Organization.ID, Sensitive: sensitive.Allowed}, s.options.QueryBudget, s.now())
switch {
case errors.Is(err, query.ErrSensitivePermissionRequired):
view.ErrorMessage = "This query requires permission to read sensitive fields."
s.renderHTML(w, r, http.StatusForbidden, site.Explore(view))
return
case errors.Is(err, query.ErrBudgetExceeded):
view.ErrorMessage = "This query exceeded its execution budget. Narrow the time window, fields, or result limit."
s.renderHTML(w, r, http.StatusUnprocessableEntity, site.Explore(view))
return
case errors.Is(err, query.ErrTypeMismatch):
view.ErrorMessage = "A query value did not match the selected field type."
s.renderHTML(w, r, http.StatusUnprocessableEntity, site.Explore(view))
return
case err != nil:
view.ErrorMessage = "The bounded query is temporarily unavailable. Your query text remains here to retry."
s.renderHTML(w, r, http.StatusServiceUnavailable, site.Explore(view))
return
}
view.Executed = true
view.Table = resultTable("Authorized query results", result)
view.Table.Empty = "No observations matched this query."
view.Stats = site.QueryStatsView{
ScannedRows: result.Stats.ScannedRows, MatchedRows: result.Stats.MatchedRows,
ScannedBytes: formatQueryBytes(result.Stats.ScannedBytes),
Duration: formatQueryDuration(result.Stats.DurationNS),
Truncated: result.Stats.Truncated, Approximate: result.Stats.Approximate,
}
s.renderHTML(w, r, http.StatusOK, site.Explore(view))
}
func (s *Server) exploreView(w http.ResponseWriter, r *http.Request, queryText string) (site.ExploreView, string, bool) {
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok {
http.Redirect(w, r, "/login/", http.StatusSeeOther)
return site.ExploreView{}, "", false
}
values := r.URL.Query()
organizationID := values.Get("organization")
if len(values) != 1 || len(values["organization"]) != 1 || organizationID == "" {
writeProblem(w, http.StatusBadRequest, "organization is required")
return site.ExploreView{}, "", false
}
organizations, err := s.identity.OrganizationsForUser(r.Context(), principal.User.ID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "organization list unavailable")
return site.ExploreView{}, "", false
}
organizationName := ""
for _, organization := range organizations {
if organization.ID == organizationID {
organizationName = organization.Name
break
}
}
if organizationName == "" {
writeProblem(w, http.StatusForbidden, "organization access denied")
return site.ExploreView{}, "", false
}
scope := access.Scope{OrganizationID: organizationID}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionTelemetryQuery)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "authorization unavailable")
return site.ExploreView{}, "", false
}
if !decision.Allowed {
writeProblem(w, http.StatusForbidden, "telemetry query access denied")
return site.ExploreView{}, "", false
}
if err = s.identity.ValidateResourceScope(r.Context(), scope); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid organization")
return site.ExploreView{}, "", false
}
token, ok := authhttp.SessionToken(r, s.cookie)
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return site.ExploreView{}, "", false
}
csrf, err := authhttp.CSRFToken(token, "query:execute")
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return site.ExploreView{}, "", false
}
view := site.ExploreView{
Head: s.head("Explore — Gamertan Observatory", "Run an authorized, bounded query against organization evidence.", "/app/explore/"),
DisplayName: principal.User.DisplayName,
Organization: site.OrganizationOption{ID: organizationID, Name: organizationName, Selected: true},
Query: queryText, CSRFToken: csrf,
EventsURL: "/app/events?organization=" + url.QueryEscape(organizationID),
}
return view, token, true
}
func formatQueryBytes(value int64) string {
if value < 1024 {
return fmt.Sprintf("%d B", value)
}
units := []string{"KiB", "MiB", "GiB", "TiB"}
amount := float64(value)
for _, unit := range units {
amount /= 1024
if amount < 1024 || unit == units[len(units)-1] {
return fmt.Sprintf("%.1f %s", amount, unit)
}
}
return fmt.Sprintf("%d B", value)
}
func formatQueryDuration(nanoseconds int64) string {
duration := time.Duration(nanoseconds)
if duration < time.Microsecond {
return duration.String()
}
if duration < time.Millisecond {
return duration.Round(time.Microsecond).String()
}
return duration.Round(time.Millisecond).String()
}
+281
View File
@@ -0,0 +1,281 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"context"
"net/http"
"net/url"
"strconv"
"time"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/site"
"gamertan.com/observatory/internal/storage"
"gamertan.com/web/access"
"gamertan.com/web/auth"
"gamertan.com/web/authhttp"
)
// EvaluateAlerts performs one bounded due-rule pass. It publishes only a
// generic organization invalidation signal when an incident changed;
// telemetry and incident details never enter the SSE stream.
func (s *Server) EvaluateAlerts(ctx context.Context) (int, error) {
evaluations, err := s.store.EvaluateDueAlertRules(ctx, s.options.QueryBudget, s.now())
if err != nil {
return 0, err
}
organizations := map[string]struct{}{}
for _, evaluation := range evaluations {
if evaluation.IncidentChanged {
organizations[evaluation.OrganizationID] = struct{}{}
}
if evaluation.IncidentChanged && evaluation.IncidentState == "firing" && s.options.PushDispatcher != nil {
s.options.PushDispatcher.Enqueue(evaluation.OrganizationID)
}
}
for organizationID := range organizations {
s.refresh.publish(organizationID)
}
return len(evaluations), nil
}
func (s *Server) incidentInbox(w http.ResponseWriter, r *http.Request) {
principal, organizationID, organizationName, ok := s.authorizeIncidentRead(w, r)
if !ok {
return
}
scope := access.Scope{OrganizationID: organizationID}
incidents, err := s.store.Incidents(r.Context(), organizationID, true, 100)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "incidents unavailable")
return
}
rules, err := s.store.AlertRules(r.Context(), organizationID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "alert rules unavailable")
return
}
saved, err := s.store.SavedQueries(r.Context(), organizationID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "saved queries unavailable")
return
}
view := site.IncidentInboxView{
Head: s.head("Incident inbox — Gamertan Observatory", "Authorized incident response and bounded alert rules.", "/app/incidents/"),
DisplayName: principal.User.DisplayName,
Organization: site.OrganizationOption{ID: organizationID, Name: organizationName, Selected: true},
EventsURL: "/app/events?organization=" + url.QueryEscape(organizationID),
OfflineURL: "/app/incidents/offline/?organization=" + url.QueryEscape(organizationID),
CacheKey: "/app/incidents/?organization=" + url.QueryEscape(organizationID),
}
if s.options.PushDispatcher != nil {
token, sessionOK := authhttp.SessionToken(r, s.cookie)
if !sessionOK {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return
}
view.PushPublicKey = s.options.PushPublicKey
view.PushCSRF, err = authhttp.CSRFToken(token, "push:manage")
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
}
for _, incident := range incidents {
item := site.IncidentSummary{ID: incident.ID, Title: incident.Title, State: incident.State, Severity: incident.Severity, StartedAt: incident.StartedAt.Format("2006-01-02 15:04:05 UTC"), UpdatedAt: incident.UpdatedAt.Format("2006-01-02 15:04:05 UTC")}
if incident.SilencedUntil != nil {
item.SilencedUntil = incident.SilencedUntil.Format("2006-01-02 15:04:05 UTC")
}
view.Incidents = append(view.Incidents, item)
if incident.State != "resolved" {
view.OpenCount++
}
}
for _, rule := range rules {
item := site.AlertRuleSummary{Name: rule.Name, Description: rule.Description, Severity: rule.Severity, Enabled: rule.Enabled, Interval: rule.EvaluationInterval.String(), LastError: rule.LastError}
if rule.LastEvaluatedAt != nil {
item.LastEvaluatedAt = rule.LastEvaluatedAt.Format("2006-01-02 15:04:05 UTC")
}
view.Rules = append(view.Rules, item)
}
for _, savedQuery := range saved {
view.SavedQueries = append(view.SavedQueries, site.SavedQuerySummary{ID: savedQuery.ID, Name: savedQuery.Name, Description: savedQuery.Description, Query: savedQuery.Query})
}
manage, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionIncidentsManage)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "authorization unavailable")
return
}
view.CanManage = manage.Allowed
if view.CanManage {
token, sessionOK := authhttp.SessionToken(r, s.cookie)
if !sessionOK {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return
}
view.ManageCSRF, err = authhttp.CSRFToken(token, "incidents:manage")
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
}
s.renderHTML(w, r, http.StatusOK, site.IncidentInbox(view))
}
func (s *Server) offlineIncidentInbox(w http.ResponseWriter, r *http.Request) {
_, organizationID, organizationName, ok := s.authorizeIncidentRead(w, r)
if !ok {
return
}
incidents, err := s.store.Incidents(r.Context(), organizationID, false, 100)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "incidents unavailable")
return
}
view := site.OfflineIncidentView{
Head: s.head("Saved incident inbox — Gamertan Observatory", "A deliberately saved read-only incident snapshot.", "/app/incidents/"),
Organization: site.OrganizationOption{ID: organizationID, Name: organizationName, Selected: true},
CapturedAt: s.now().Format("2006-01-02 15:04:05 UTC"),
}
for _, incident := range incidents {
item := site.IncidentSummary{Title: incident.Title, State: incident.State, Severity: incident.Severity, StartedAt: incident.StartedAt.Format("2006-01-02 15:04:05 UTC"), UpdatedAt: incident.UpdatedAt.Format("2006-01-02 15:04:05 UTC")}
if incident.SilencedUntil != nil {
item.SilencedUntil = incident.SilencedUntil.Format("2006-01-02 15:04:05 UTC")
}
view.Incidents = append(view.Incidents, item)
}
s.renderHTML(w, r, http.StatusOK, site.OfflineIncidentInbox(view))
}
func (s *Server) authorizeIncidentRead(w http.ResponseWriter, r *http.Request) (auth.Principal, string, string, bool) {
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok {
http.Redirect(w, r, "/login/", http.StatusSeeOther)
return auth.Principal{}, "", "", false
}
values := r.URL.Query()
organizationValues, exists := values["organization"]
if !exists || len(values) != 1 || len(organizationValues) != 1 || organizationValues[0] == "" {
writeProblem(w, http.StatusBadRequest, "incident organization is required")
return auth.Principal{}, "", "", false
}
organizationID := organizationValues[0]
scope := access.Scope{OrganizationID: organizationID}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionIncidentsRead)
if err != nil || !decision.Allowed {
writeProblem(w, http.StatusForbidden, "incident access denied")
return auth.Principal{}, "", "", false
}
if err = s.identity.ValidateResourceScope(r.Context(), scope); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid organization")
return auth.Principal{}, "", "", false
}
organizations, err := s.identity.OrganizationsForUser(r.Context(), principal.User.ID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "organization unavailable")
return auth.Principal{}, "", "", false
}
for _, organization := range organizations {
if organization.ID == organizationID {
return principal, organizationID, organization.Name, true
}
}
writeProblem(w, http.StatusForbidden, "incident access denied")
return auth.Principal{}, "", "", false
}
func (s *Server) createAlertRule(w http.ResponseWriter, r *http.Request) {
values, principal, ok := s.authorizeIncidentForm(w, r, []string{"organization_id", "csrf_token", "name", "description", "saved_query_id", "severity", "minimum_matches", "required_consecutive", "evaluation_interval"}, nil)
if !ok {
return
}
minimumMatches, err := strconv.Atoi(values.Get("minimum_matches"))
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "alert rule rejected")
return
}
requiredConsecutive, err := strconv.Atoi(values.Get("required_consecutive"))
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "alert rule rejected")
return
}
intervals := map[string]time.Duration{"15s": 15 * time.Second, "30s": 30 * time.Second, "1m": time.Minute, "5m": 5 * time.Minute, "15m": 15 * time.Minute}
interval, exists := intervals[values.Get("evaluation_interval")]
if !exists {
writeProblem(w, http.StatusUnprocessableEntity, "alert rule rejected")
return
}
_, err = s.store.SaveAlertRule(r.Context(), storage.AlertRuleInput{
OrganizationID: values.Get("organization_id"), Name: values.Get("name"), Description: values.Get("description"),
SavedQueryID: values.Get("saved_query_id"), Severity: values.Get("severity"), MinimumMatches: minimumMatches,
RequiredConsecutive: requiredConsecutive, EvaluationInterval: interval, Enabled: true, ActorUserID: principal.User.ID,
}, s.now())
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "alert rule rejected")
return
}
http.Redirect(w, r, "/app/incidents/?organization="+url.QueryEscape(values.Get("organization_id")), http.StatusSeeOther)
}
func (s *Server) transitionIncident(w http.ResponseWriter, r *http.Request) {
values, principal, ok := s.authorizeIncidentForm(w, r, []string{"organization_id", "csrf_token", "action"}, []string{"silence_duration"})
if !ok {
return
}
var silenceUntil *time.Time
if values.Get("action") == "silence" {
durations := map[string]time.Duration{"15m": 15 * time.Minute, "1h": time.Hour, "6h": 6 * time.Hour, "24h": 24 * time.Hour, "168h": 7 * 24 * time.Hour}
duration, exists := durations[values.Get("silence_duration")]
if !exists {
writeProblem(w, http.StatusUnprocessableEntity, "incident transition rejected")
return
}
until := s.now().Add(duration)
silenceUntil = &until
} else if values.Get("silence_duration") != "" {
writeProblem(w, http.StatusUnprocessableEntity, "incident transition rejected")
return
}
_, err := s.store.TransitionIncident(r.Context(), values.Get("organization_id"), r.PathValue("id"), values.Get("action"), principal.User.ID, silenceUntil, s.now())
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "incident transition rejected")
return
}
s.refresh.publish(values.Get("organization_id"))
http.Redirect(w, r, "/app/incidents/?organization="+url.QueryEscape(values.Get("organization_id")), http.StatusSeeOther)
}
func (s *Server) authorizeIncidentForm(w http.ResponseWriter, r *http.Request, required, optional []string) (url.Values, auth.Principal, bool) {
principal, ok := auth.PrincipalFromContext(r.Context())
values, err := readFormFields(w, r, 24<<10, required, optional)
token, sessionOK := authhttp.SessionToken(r, s.cookie)
csrfOK := err == nil && sessionOK && authhttp.VerifyCSRF(token, "incidents:manage", values.Get("csrf_token"))
if !tokenBoundFormRequest(r, s.options.PublicOrigin, csrfOK) {
writeProblem(w, http.StatusForbidden, "valid incident form required")
return nil, auth.Principal{}, false
}
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return nil, auth.Principal{}, false
}
if err != nil {
writeProblem(w, http.StatusBadRequest, "invalid incident management request")
return nil, auth.Principal{}, false
}
scope := access.Scope{OrganizationID: values.Get("organization_id")}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionIncidentsManage)
if err != nil || !decision.Allowed {
writeProblem(w, http.StatusForbidden, "incident management denied")
return nil, auth.Principal{}, false
}
if err = s.identity.ValidateResourceScope(r.Context(), scope); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid organization")
return nil, auth.Principal{}, false
}
if !csrfOK {
writeProblem(w, http.StatusForbidden, "valid incident CSRF token required")
return nil, auth.Principal{}, false
}
return values, principal, true
}
+591
View File
@@ -0,0 +1,591 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
"net/http"
"net/url"
"strconv"
"strings"
"unicode/utf8"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/query"
"gamertan.com/observatory/internal/site"
"gamertan.com/observatory/internal/storage"
"gamertan.com/web/access"
"gamertan.com/web/auth"
"gamertan.com/web/authhttp"
)
func (s *Server) createSavedQuery(w http.ResponseWriter, r *http.Request) {
values, principal, ok := s.authorizeManagementForm(w, r, "organization_id", "csrf_token", "name", "description", "query")
if !ok {
return
}
s.saveQuery(w, r, values, principal, values.Get("query"))
}
func (s *Server) createBuiltQuery(w http.ResponseWriter, r *http.Request) {
required := []string{"organization_id", "csrf_token", "name", "description", "signal", "filter_operator", "window", "aggregate", "limit"}
optional := []string{"filter_field", "filter_value", "aggregate_field", "group_by", "bucket"}
values, principal, ok := s.authorizeManagementFormFields(w, r, required, optional)
if !ok {
return
}
text, err := buildAssistedQuery(values, s.options.MaxQueryRows)
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "assisted query rejected")
return
}
s.saveQuery(w, r, values, principal, text)
}
func (s *Server) saveQuery(w http.ResponseWriter, r *http.Request, values url.Values, principal auth.Principal, text string) {
_, err := s.store.SaveQuery(r.Context(), storage.SavedQueryInput{
OrganizationID: values.Get("organization_id"), Name: values.Get("name"),
Description: values.Get("description"), Query: text,
ActorUserID: principal.User.ID, MaxRows: s.options.MaxQueryRows,
}, s.now())
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "saved query rejected")
return
}
http.Redirect(w, r, "/app/?organization="+url.QueryEscape(values.Get("organization_id"))+"#saved-work", http.StatusSeeOther)
}
func buildAssistedQuery(values url.Values, maxRows int) (string, error) {
allowed := func(value string, candidates ...string) bool {
for _, candidate := range candidates {
if value == candidate {
return true
}
}
return false
}
signal := values.Get("signal")
if !allowed(signal, "logs", "metrics", "traces", "deployments") {
return "", fmt.Errorf("unsupported signal")
}
window := values.Get("window")
if !allowed(window, "15m", "1h", "6h", "24h", "168h") {
return "", fmt.Errorf("unsupported window")
}
limit, err := strconv.Atoi(values.Get("limit"))
if err != nil || limit < 1 || limit > maxRows || !allowed(values.Get("limit"), "10", "20", "50", "100", "250") {
return "", fmt.Errorf("unsupported limit")
}
stages := []string{signal}
filterField := values.Get("filter_field")
filterValue := values.Get("filter_value")
filterOperator := values.Get("filter_operator")
if !allowed(filterOperator, "==", "!=", ">=", "<=", ">", "<") {
return "", fmt.Errorf("invalid filter comparison")
}
if filterField == "" {
if filterValue != "" {
return "", fmt.Errorf("filter value requires a field")
}
} else {
if !allowed(filterField, "service", "project", "environment", "route", "status", "duration", "name", "severity", "value", "trace_id", "correlation_id") ||
filterValue == "" || len(filterValue) > 256 || !utf8.ValidString(filterValue) || strings.IndexByte(filterValue, 0) >= 0 {
return "", fmt.Errorf("invalid filter")
}
quoted := strconv.Quote(filterValue)
quoted = strings.ReplaceAll(quoted, "|", `\u007c`)
stages = append(stages, "where "+filterField+" "+filterOperator+" "+quoted)
}
stages = append(stages, "window "+window)
aggregate := values.Get("aggregate")
aggregateField := values.Get("aggregate_field")
groupBy := values.Get("group_by")
bucket := values.Get("bucket")
if aggregate == "none" {
if aggregateField != "" || groupBy != "" || bucket != "" {
return "", fmt.Errorf("summary options require an aggregate")
}
} else {
if !allowed(aggregate, "count", "min", "max", "sum", "avg", "p50", "p95", "p99") ||
!allowed(groupBy, "", "service", "project", "environment", "route", "status", "name", "severity") ||
!allowed(bucket, "", "1m", "5m", "15m", "1h") {
return "", fmt.Errorf("invalid summary")
}
expression := "count()"
if aggregate == "count" {
if aggregateField != "" {
return "", fmt.Errorf("count accepts no field")
}
} else {
if !allowed(aggregateField, "value", "duration", "status") {
return "", fmt.Errorf("numeric aggregate field required")
}
expression = aggregate + "(" + aggregateField + ")"
}
groups := make([]string, 0, 2)
if groupBy != "" {
groups = append(groups, groupBy)
}
if bucket != "" {
groups = append(groups, "window("+bucket+")")
}
stage := "summarize " + expression
if len(groups) > 0 {
stage += " by " + strings.Join(groups, ", ")
}
stages = append(stages, stage)
}
stages = append(stages, "limit "+strconv.Itoa(limit))
text := strings.Join(stages, " | ")
if _, err = query.Parse(text, maxRows); err != nil {
return "", err
}
return text, nil
}
func (s *Server) createDashboard(w http.ResponseWriter, r *http.Request) {
values, principal, ok := s.authorizeManagementForm(w, r, "organization_id", "csrf_token", "slug", "name", "description", "panel_title", "saved_query_id", "visualization")
if !ok {
return
}
organizationID := values.Get("organization_id")
queryValue, err := s.store.SavedQuery(r.Context(), organizationID, values.Get("saved_query_id"))
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "dashboard query rejected")
return
}
visualization := values.Get("visualization")
if !validDashboardPresentation(queryValue, visualization) {
writeProblem(w, http.StatusUnprocessableEntity, "dashboard presentation does not match query")
return
}
_, err = s.store.SaveDashboard(r.Context(), storage.DashboardInput{
OrganizationID: organizationID, Slug: values.Get("slug"), Name: values.Get("name"),
Description: values.Get("description"), ActorUserID: principal.User.ID,
Panels: []storage.DashboardPanel{{Position: 0, Title: values.Get("panel_title"), Visualization: visualization, SavedQueryID: queryValue.ID}},
}, s.now())
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "dashboard rejected")
return
}
http.Redirect(w, r, "/app/dashboards/"+url.PathEscape(values.Get("slug"))+"/?organization="+url.QueryEscape(organizationID), http.StatusSeeOther)
}
func (s *Server) updateDashboard(w http.ResponseWriter, r *http.Request) {
values, principal, ok := s.authorizeManagementForm(w, r, "organization_id", "csrf_token", "dashboard_id", "expected_revision", "slug", "name", "description")
if !ok {
return
}
current, ok := s.dashboardForRevision(w, r, values)
if !ok {
return
}
updated, err := s.store.SaveDashboard(r.Context(), storage.DashboardInput{
ID: current.ID, ExpectedRevision: current.Revision, OrganizationID: current.OrganizationID,
Slug: values.Get("slug"), Name: values.Get("name"), Description: values.Get("description"),
Panels: current.Panels, ActorUserID: principal.User.ID,
}, s.now())
if !writeDashboardRevisionResult(w, err) {
return
}
redirectDashboard(w, r, updated.Slug, current.OrganizationID)
}
func (s *Server) addDashboardPanel(w http.ResponseWriter, r *http.Request) {
values, principal, ok := s.authorizeManagementForm(w, r, "organization_id", "csrf_token", "dashboard_id", "expected_revision", "panel_title", "saved_query_id", "visualization")
if !ok {
return
}
current, ok := s.dashboardForRevision(w, r, values)
if !ok {
return
}
queryValue, err := s.store.SavedQuery(r.Context(), current.OrganizationID, values.Get("saved_query_id"))
if err != nil || !validDashboardPresentation(queryValue, values.Get("visualization")) {
writeProblem(w, http.StatusUnprocessableEntity, "dashboard panel rejected")
return
}
panels := append([]storage.DashboardPanel(nil), current.Panels...)
panels = append(panels, storage.DashboardPanel{Position: len(panels), Title: values.Get("panel_title"), Visualization: values.Get("visualization"), SavedQueryID: queryValue.ID})
updated, err := s.store.SaveDashboard(r.Context(), storage.DashboardInput{
ID: current.ID, ExpectedRevision: current.Revision, OrganizationID: current.OrganizationID,
Slug: current.Slug, Name: current.Name, Description: current.Description,
Panels: panels, ActorUserID: principal.User.ID,
}, s.now())
if !writeDashboardRevisionResult(w, err) {
return
}
redirectDashboard(w, r, updated.Slug, current.OrganizationID)
}
func (s *Server) updateDashboardPanel(w http.ResponseWriter, r *http.Request) {
values, principal, ok := s.authorizeManagementForm(w, r, "organization_id", "csrf_token", "dashboard_id", "expected_revision", "panel_title", "saved_query_id", "visualization")
if !ok {
return
}
current, ok := s.dashboardForRevision(w, r, values)
if !ok {
return
}
queryValue, err := s.store.SavedQuery(r.Context(), current.OrganizationID, values.Get("saved_query_id"))
if err != nil || !validDashboardPresentation(queryValue, values.Get("visualization")) {
writeProblem(w, http.StatusUnprocessableEntity, "dashboard panel rejected")
return
}
panelID := r.PathValue("panel")
panels := append([]storage.DashboardPanel(nil), current.Panels...)
found := false
for index := range panels {
if panels[index].ID != panelID {
continue
}
panels[index].Title = values.Get("panel_title")
panels[index].Visualization = values.Get("visualization")
panels[index].SavedQueryID = queryValue.ID
found = true
break
}
if !found {
writeProblem(w, http.StatusNotFound, "dashboard panel not found")
return
}
updated, err := s.store.SaveDashboard(r.Context(), storage.DashboardInput{
ID: current.ID, ExpectedRevision: current.Revision, OrganizationID: current.OrganizationID,
Slug: current.Slug, Name: current.Name, Description: current.Description,
Panels: panels, ActorUserID: principal.User.ID,
}, s.now())
if !writeDashboardRevisionResult(w, err) {
return
}
redirectDashboard(w, r, updated.Slug, current.OrganizationID)
}
func (s *Server) removeDashboardPanel(w http.ResponseWriter, r *http.Request) {
values, principal, ok := s.authorizeManagementForm(w, r, "organization_id", "csrf_token", "dashboard_id", "expected_revision")
if !ok {
return
}
current, ok := s.dashboardForRevision(w, r, values)
if !ok {
return
}
panelID := r.PathValue("panel")
panels := make([]storage.DashboardPanel, 0, len(current.Panels))
for _, panel := range current.Panels {
if panel.ID != panelID {
panel.Position = len(panels)
panels = append(panels, panel)
}
}
if len(panels) == len(current.Panels) {
writeProblem(w, http.StatusNotFound, "dashboard panel not found")
return
}
updated, err := s.store.SaveDashboard(r.Context(), storage.DashboardInput{
ID: current.ID, ExpectedRevision: current.Revision, OrganizationID: current.OrganizationID,
Slug: current.Slug, Name: current.Name, Description: current.Description,
Panels: panels, ActorUserID: principal.User.ID,
}, s.now())
if !writeDashboardRevisionResult(w, err) {
return
}
redirectDashboard(w, r, updated.Slug, current.OrganizationID)
}
func (s *Server) dashboardForRevision(w http.ResponseWriter, r *http.Request, values url.Values) (storage.Dashboard, bool) {
current, err := s.store.Dashboard(r.Context(), values.Get("organization_id"), r.PathValue("slug"))
if err != nil {
writeProblem(w, http.StatusNotFound, "dashboard not found")
return storage.Dashboard{}, false
}
revision, err := strconv.Atoi(values.Get("expected_revision"))
if err != nil || revision < 1 || values.Get("dashboard_id") != current.ID {
writeProblem(w, http.StatusBadRequest, "dashboard revision is invalid")
return storage.Dashboard{}, false
}
if revision != current.Revision {
writeProblem(w, http.StatusConflict, "dashboard changed; reload before editing")
return storage.Dashboard{}, false
}
return current, true
}
func validDashboardPresentation(saved storage.SavedQuery, visualization string) bool {
switch visualization {
case "table":
return true
case "stat":
return saved.AST.Summary != nil
case "timeseries":
return saved.AST.Summary != nil && saved.AST.Bucket > 0
default:
return false
}
}
func writeDashboardRevisionResult(w http.ResponseWriter, err error) bool {
if err == nil {
return true
}
if errors.Is(err, storage.ErrDashboardRevisionConflict) {
writeProblem(w, http.StatusConflict, "dashboard changed; reload before editing")
} else {
writeProblem(w, http.StatusUnprocessableEntity, "dashboard revision rejected")
}
return false
}
func redirectDashboard(w http.ResponseWriter, r *http.Request, slug, organizationID string) {
http.Redirect(w, r, "/app/dashboards/"+url.PathEscape(slug)+"/?organization="+url.QueryEscape(organizationID), http.StatusSeeOther)
}
func (s *Server) authorizeManagementForm(w http.ResponseWriter, r *http.Request, fields ...string) (url.Values, auth.Principal, bool) {
return s.authorizeManagementFormFields(w, r, fields, nil)
}
func (s *Server) authorizeManagementFormFields(w http.ResponseWriter, r *http.Request, required, optional []string) (url.Values, auth.Principal, bool) {
principal, ok := auth.PrincipalFromContext(r.Context())
values, err := readFormFields(w, r, 24<<10, required, optional)
token, sessionOK := authhttp.SessionToken(r, s.cookie)
csrfOK := err == nil && sessionOK && authhttp.VerifyCSRF(token, "dashboards:manage", values.Get("csrf_token"))
if !tokenBoundFormRequest(r, s.options.PublicOrigin, csrfOK) {
writeProblem(w, http.StatusForbidden, "valid dashboard form required")
return nil, auth.Principal{}, false
}
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return nil, auth.Principal{}, false
}
if err != nil {
writeProblem(w, http.StatusBadRequest, "invalid dashboard management request")
return nil, auth.Principal{}, false
}
organizationID := values.Get("organization_id")
scope := access.Scope{OrganizationID: organizationID}
if err = s.identity.ValidateResourceScope(r.Context(), scope); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid organization")
return nil, auth.Principal{}, false
}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionDashboardsManage)
if err != nil || !decision.Allowed {
writeProblem(w, http.StatusForbidden, "dashboard management denied")
return nil, auth.Principal{}, false
}
if !csrfOK {
writeProblem(w, http.StatusForbidden, "valid dashboard CSRF token required")
return nil, auth.Principal{}, false
}
return values, principal, true
}
func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) {
organizationID, principal, ok := s.authorizeDashboardRead(w, r)
if !ok {
return
}
dashboard, err := s.store.Dashboard(r.Context(), organizationID, r.PathValue("slug"))
if err != nil {
writeProblem(w, http.StatusNotFound, "dashboard not found")
return
}
organizations, err := s.identity.OrganizationsForUser(r.Context(), principal.User.ID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "organization unavailable")
return
}
organizationName := "Organization"
for _, organization := range organizations {
if organization.ID == organizationID {
organizationName = organization.Name
break
}
}
view := site.DashboardView{
Head: s.head(dashboard.Name+" — Gamertan Observatory", dashboard.Description, "/app/dashboards/"+url.PathEscape(dashboard.Slug)+"/"),
DisplayName: principal.User.DisplayName, Organization: site.OrganizationOption{ID: organizationID, Name: organizationName, Selected: true},
ID: dashboard.ID, Slug: dashboard.Slug, Revision: dashboard.Revision,
Name: dashboard.Name, Description: dashboard.Description,
ExportURL: "/app/dashboards/" + url.PathEscape(dashboard.Slug) + "/export.json?organization=" + url.QueryEscape(organizationID),
}
manage, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, access.Scope{OrganizationID: organizationID}, identity.PermissionDashboardsManage)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "authorization unavailable")
return
}
view.CanManage = manage.Allowed
if view.CanManage {
token, sessionOK := authhttp.SessionToken(r, s.cookie)
if !sessionOK {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return
}
view.ManageCSRF, err = authhttp.CSRFToken(token, "dashboards:manage")
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
savedQueries, loadErr := s.store.SavedQueries(r.Context(), organizationID)
if loadErr != nil {
writeProblem(w, http.StatusServiceUnavailable, "saved queries unavailable")
return
}
for _, saved := range savedQueries {
view.SavedQueries = append(view.SavedQueries, site.SavedQuerySummary{ID: saved.ID, Name: saved.Name, Description: saved.Description, Query: saved.Query})
}
}
for _, panel := range dashboard.Panels {
view.Panels = append(view.Panels, s.dashboardPanel(r, principal.User.ID, organizationID, panel))
}
s.renderHTML(w, r, http.StatusOK, site.Dashboard(view))
}
func (s *Server) dashboardPanel(r *http.Request, userID, organizationID string, panel storage.DashboardPanel) site.PanelView {
view := site.PanelView{ID: panel.ID, SavedQueryID: panel.SavedQueryID, Title: panel.Title, Visualization: panel.Visualization, Table: site.TableView{Caption: panel.Title, Columns: []site.TableColumn{{Label: "Status"}}, Empty: "Panel data is unavailable."}}
saved, err := s.store.SavedQuery(r.Context(), organizationID, panel.SavedQueryID)
if err != nil {
return view
}
view.Query = saved.Query
scope := access.Scope{OrganizationID: organizationID, ProjectID: saved.Scope.ProjectID, EnvironmentID: saved.Scope.EnvironmentID, ServiceID: saved.Scope.ServiceID}
decision, err := s.identity.Access.Authorize(r.Context(), userID, scope, identity.PermissionTelemetryQuery)
if err != nil || !decision.Allowed || s.identity.ValidateResourceScope(r.Context(), scope) != nil {
return view
}
sensitive, err := s.identity.Access.Authorize(r.Context(), userID, scope, identity.PermissionTelemetryReadSensitive)
if err != nil {
return view
}
result, err := s.store.Query(r.Context(), saved.AST, query.Scope{OrganizationID: organizationID, ProjectID: saved.Scope.ProjectID, EnvironmentID: saved.Scope.EnvironmentID, ServiceID: saved.Scope.ServiceID, Sensitive: sensitive.Allowed}, s.options.QueryBudget, s.now())
if err != nil {
return view
}
view.Table = resultTable(panel.Title, result)
if panel.Visualization == "timeseries" {
view.Chart = resultChart(panel.Title, result)
}
if panel.Visualization == "stat" {
for _, row := range result.Rows {
for _, value := range row.Values {
if value != nil {
view.Stat = boundedCell(*value)
return view
}
}
}
}
return view
}
func resultChart(title string, result query.Result) site.ChartView {
if len(result.Columns) < 2 || len(result.Rows) == 0 {
return site.ChartView{}
}
valueIndex := len(result.Columns) - 1
valueType := result.Columns[valueIndex].Type
if valueType != "integer" && valueType != "float" && valueType != "duration" {
return site.ChartView{}
}
const maxPoints = 48
points := make([]struct {
label, display string
value float64
}, 0, min(len(result.Rows), maxPoints))
maximum := float64(0)
for index, row := range result.Rows {
if index >= maxPoints || valueIndex >= len(row.Values) || row.Values[valueIndex] == nil {
continue
}
value, err := strconv.ParseFloat(*row.Values[valueIndex], 64)
if err != nil || math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
return site.ChartView{}
}
labels := make([]string, 0, valueIndex)
for column := 0; column < valueIndex && column < len(row.Values); column++ {
if row.Values[column] != nil {
labels = append(labels, boundedCell(*row.Values[column]))
}
}
label := boundedCell(strings.Join(labels, " · "))
if label == "" {
label = fmt.Sprintf("Point %d", index+1)
}
display := boundedCell(*row.Values[valueIndex])
if unit := result.Columns[valueIndex].Unit; unit != "" {
display += " " + boundedCell(unit)
}
points = append(points, struct {
label, display string
value float64
}{label: label, display: display, value: value})
maximum = math.Max(maximum, value)
}
if len(points) == 0 {
return site.ChartView{}
}
if maximum == 0 {
maximum = 1
}
view := site.ChartView{Label: title + " visual summary"}
for _, point := range points {
view.Points = append(view.Points, site.ChartPoint{
Label: point.label, Value: strconv.FormatFloat(point.value, 'g', -1, 64),
Maximum: strconv.FormatFloat(maximum, 'g', -1, 64), Display: point.display,
})
}
return view
}
func (s *Server) exportDashboard(w http.ResponseWriter, r *http.Request) {
organizationID, _, ok := s.authorizeDashboardRead(w, r)
if !ok {
return
}
exported, err := s.store.ExportDashboard(r.Context(), organizationID, r.PathValue("slug"))
if err != nil {
writeProblem(w, http.StatusNotFound, "dashboard not found")
return
}
var body bytes.Buffer
encoder := json.NewEncoder(&body)
encoder.SetIndent("", " ")
if err = encoder.Encode(exported); err != nil {
writeProblem(w, http.StatusInternalServerError, "dashboard export unavailable")
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", "observatory-dashboard-"+r.PathValue("slug")+".json"))
w.Header().Set("Content-Length", fmt.Sprintf("%d", body.Len()))
if r.Method != http.MethodHead {
_, _ = w.Write(body.Bytes())
}
}
func (s *Server) authorizeDashboardRead(w http.ResponseWriter, r *http.Request) (string, auth.Principal, bool) {
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return "", auth.Principal{}, false
}
values := r.URL.Query()
organizationID := values.Get("organization")
if len(values) != 1 || len(values["organization"]) != 1 || organizationID == "" {
writeProblem(w, http.StatusBadRequest, "organization is required")
return "", auth.Principal{}, false
}
scope := access.Scope{OrganizationID: organizationID}
if err := s.identity.ValidateResourceScope(r.Context(), scope); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid organization")
return "", auth.Principal{}, false
}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionDashboardsRead)
if err != nil || !decision.Allowed {
writeProblem(w, http.StatusForbidden, "dashboard access denied")
return "", auth.Principal{}, false
}
return organizationID, principal, true
}
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/model"
"gamertan.com/observatory/internal/nativeprotocol"
"gamertan.com/observatory/internal/storage"
)
func BenchmarkNativeExactReplay(b *testing.B) {
root := filepath.Join(b.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
b.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
b.Fatal(err)
}
defer store.Close()
identities, err := identity.Open(root)
if err != nil {
b.Fatal(err)
}
defer identities.Close()
token, err := store.CreateSource(context.Background(), "source", model.Scope{OrganizationID: "org", ProjectID: "project", EnvironmentID: "production", ServiceID: "service"})
if err != nil {
b.Fatal(err)
}
now := time.Date(2026, 8, 18, 20, 0, 0, 0, time.UTC)
server, err := New(store, identities, testOptions())
if err != nil {
b.Fatal(err)
}
server.now = func() time.Time { return now }
handler := server.Handler()
legacy := model.Batch{Version: model.BatchVersion, SourceID: "source", StreamID: "legacy", Sequence: 1, ObservedAt: now, Signal: model.SignalLogs, Records: benchmarkRecords(now)}
legacyBody, _ := json.Marshal(legacy)
framed := legacy
framed.StreamID = "framed"
framedBody, _ := json.Marshal(framed)
framedEnvelope, _ := framed.Envelope(framedBody)
seed := func(path string, body []byte, envelope *model.BatchEnvelope) {
request := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(body))
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("Content-Type", "application/json")
if envelope != nil {
nativeprotocol.SetHeaders(request.Header, *envelope)
}
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusAccepted {
b.Fatalf("seed %s: %d %s", path, response.Code, response.Body.String())
}
}
seed("/api/v1/ingest/native", legacyBody, nil)
seed("/api/v2/ingest/native", framedBody, &framedEnvelope)
for _, benchmark := range []struct {
name string
path string
body []byte
envelope *model.BatchEnvelope
}{{"legacy-v1", "/api/v1/ingest/native", legacyBody, nil}, {"framed-v2", "/api/v2/ingest/native", framedBody, &framedEnvelope}} {
b.Run(benchmark.name, func(b *testing.B) {
b.ReportAllocs()
b.SetBytes(int64(len(benchmark.body)))
for range b.N {
request := httptest.NewRequest(http.MethodPost, benchmark.path, bytes.NewReader(benchmark.body))
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("Content-Type", "application/json")
if benchmark.envelope != nil {
nativeprotocol.SetHeaders(request.Header, *benchmark.envelope)
}
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusAccepted {
b.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
})
}
}
func benchmarkRecords(now time.Time) []model.Observation {
records := make([]model.Observation, 500)
for index := range records {
records[index] = model.Observation{Timestamp: now, Name: "http.request", Attributes: map[string]string{"route": "/items", "status": "200", "method": "GET"}}
}
return records
}
+132
View File
@@ -0,0 +1,132 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"crypto/ecdh"
"encoding/base64"
"net/http"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/storage"
"gamertan.com/observatory/internal/webpush"
"gamertan.com/web/access"
"gamertan.com/web/auth"
"gamertan.com/web/authhttp"
"gamertan.com/web/websec"
)
type pushSubscriptionRequest struct {
OrganizationID string `json:"organization_id"`
Endpoint string `json:"endpoint"`
Keys struct {
P256DH string `json:"p256dh"`
Auth string `json:"auth"`
} `json:"keys"`
}
func (s *Server) savePushSubscription(w http.ResponseWriter, r *http.Request) {
request, principal, ok := s.authorizePushRequest(w, r)
if !ok {
return
}
p256dh, p256Err := base64.RawURLEncoding.DecodeString(request.Keys.P256DH)
authSecret, authErr := base64.RawURLEncoding.DecodeString(request.Keys.Auth)
if p256Err != nil || authErr != nil || len(p256dh) != 65 || len(authSecret) != 16 {
writeProblem(w, http.StatusUnprocessableEntity, "push subscription rejected")
return
}
if _, err := ecdh.P256().NewPublicKey(p256dh); err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "push subscription rejected")
return
}
if _, err := webpush.ValidateEndpoint(request.Endpoint); err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "push subscription rejected")
return
}
subscription, err := s.store.SavePushSubscription(r.Context(), storage.PushSubscriptionInput{OrganizationID: request.OrganizationID, UserID: principal.User.ID, Endpoint: request.Endpoint, P256DH: p256dh, Auth: authSecret}, s.now())
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "push subscription rejected")
return
}
writeJSON(w, http.StatusCreated, struct {
ID string `json:"id"`
}{subscription.ID})
}
func (s *Server) deletePushSubscription(w http.ResponseWriter, r *http.Request) {
request, principal, ok := s.authorizePushRequest(w, r)
if !ok {
return
}
if _, err := webpush.ValidateEndpoint(request.Endpoint); err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "push subscription rejected")
return
}
remaining, err := s.store.DeletePushSubscription(r.Context(), request.OrganizationID, principal.User.ID, request.Endpoint)
if err != nil {
writeProblem(w, http.StatusNotFound, "push subscription not found")
return
}
writeJSON(w, http.StatusOK, struct {
Remaining bool `json:"remaining"`
}{remaining})
}
func (s *Server) pushSubscriptionStatus(w http.ResponseWriter, r *http.Request) {
request, principal, ok := s.authorizePushRequest(w, r)
if !ok {
return
}
if _, err := webpush.ValidateEndpoint(request.Endpoint); err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "push subscription rejected")
return
}
subscribed, err := s.store.HasPushSubscription(r.Context(), request.OrganizationID, principal.User.ID, request.Endpoint)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "push subscription status unavailable")
return
}
writeJSON(w, http.StatusOK, struct {
Subscribed bool `json:"subscribed"`
}{subscribed})
}
func (s *Server) authorizePushRequest(w http.ResponseWriter, r *http.Request) (pushSubscriptionRequest, auth.Principal, bool) {
if s.options.PushDispatcher == nil {
writeProblem(w, http.StatusNotFound, "Web Push is not configured")
return pushSubscriptionRequest{}, auth.Principal{}, false
}
if !websec.SameOrigin(r, s.options.PublicOrigin) {
writeProblem(w, http.StatusForbidden, "same-origin request required")
return pushSubscriptionRequest{}, auth.Principal{}, false
}
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return pushSubscriptionRequest{}, auth.Principal{}, false
}
body := http.MaxBytesReader(w, r.Body, 8<<10)
defer body.Close()
var request pushSubscriptionRequest
if err := decodeOne(body, &request); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid push subscription request")
return pushSubscriptionRequest{}, auth.Principal{}, false
}
scope := access.Scope{OrganizationID: request.OrganizationID}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionIncidentsRead)
if err != nil || !decision.Allowed {
writeProblem(w, http.StatusForbidden, "incident access denied")
return pushSubscriptionRequest{}, auth.Principal{}, false
}
if err = s.identity.ValidateResourceScope(r.Context(), scope); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid organization")
return pushSubscriptionRequest{}, auth.Principal{}, false
}
token, sessionOK := authhttp.SessionToken(r, s.cookie)
if !sessionOK || !authhttp.VerifyCSRF(token, "push:manage", r.Header.Get("X-CSRF-Token")) {
writeProblem(w, http.StatusForbidden, "valid push CSRF token required")
return pushSubscriptionRequest{}, auth.Principal{}, false
}
return request, principal, true
}
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"fmt"
"net/http"
"gamertan.com/observatory/internal/site"
)
func (s *Server) webManifest(w http.ResponseWriter, r *http.Request) {
serveFixedBody(w, r, site.WebManifest(), "application/manifest+json")
}
func (s *Server) serviceWorker(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Service-Worker-Allowed", "/")
serveFixedBody(w, r, site.ServiceWorker(), "text/javascript; charset=utf-8")
}
func (s *Server) offlineShell(w http.ResponseWriter, r *http.Request) {
s.renderHTML(w, r, http.StatusOK, site.Offline(site.OfflineView{Head: s.head("Offline — Gamertan Observatory", "Observatory is temporarily unreachable.", "/offline/")}))
}
func serveFixedBody(w http.ResponseWriter, r *http.Request, body []byte, contentType string) {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
if r.Method != http.MethodHead {
_, _ = w.Write(body)
}
}
+71
View File
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"errors"
"sync"
)
var errRefreshCapacity = errors.New("live refresh capacity reached")
// refreshHub carries only a coalescible invalidation signal. It never carries
// telemetry, resource names, incident details, or credentials.
type refreshHub struct {
mu sync.Mutex
next uint64
total int
maxTotal int
maxPerOrganization int
subscribers map[string]map[uint64]chan struct{}
}
func newRefreshHub(maxTotal, maxPerOrganization int) *refreshHub {
return &refreshHub{maxTotal: maxTotal, maxPerOrganization: maxPerOrganization, subscribers: make(map[string]map[uint64]chan struct{})}
}
func (hub *refreshHub) subscribe(organizationID string) (<-chan struct{}, func(), error) {
hub.mu.Lock()
defer hub.mu.Unlock()
group := hub.subscribers[organizationID]
if hub.total >= hub.maxTotal || len(group) >= hub.maxPerOrganization {
return nil, nil, errRefreshCapacity
}
if group == nil {
group = make(map[uint64]chan struct{})
hub.subscribers[organizationID] = group
}
hub.next++
id := hub.next
updates := make(chan struct{}, 1)
group[id] = updates
hub.total++
var once sync.Once
remove := func() {
once.Do(func() {
hub.mu.Lock()
defer hub.mu.Unlock()
if current := hub.subscribers[organizationID]; current != nil {
if _, exists := current[id]; exists {
delete(current, id)
hub.total--
}
if len(current) == 0 {
delete(hub.subscribers, organizationID)
}
}
})
}
return updates, remove, nil
}
func (hub *refreshHub) publish(organizationID string) {
hub.mu.Lock()
defer hub.mu.Unlock()
for _, updates := range hub.subscribers[organizationID] {
select {
case updates <- struct{}{}:
default:
}
}
}
+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"errors"
"testing"
)
func TestRefreshHubIsBoundedCoalescedAndOrganizationScoped(t *testing.T) {
hub := newRefreshHub(2, 1)
first, removeFirst, err := hub.subscribe("organization-a")
if err != nil {
t.Fatal(err)
}
defer removeFirst()
if _, _, err = hub.subscribe("organization-a"); !errors.Is(err, errRefreshCapacity) {
t.Fatalf("same-organization capacity err=%v", err)
}
second, removeSecond, err := hub.subscribe("organization-b")
if err != nil {
t.Fatal(err)
}
defer removeSecond()
if _, _, err = hub.subscribe("organization-c"); !errors.Is(err, errRefreshCapacity) {
t.Fatalf("total capacity err=%v", err)
}
hub.publish("organization-a")
hub.publish("organization-a")
select {
case <-first:
default:
t.Fatal("organization A did not receive refresh")
}
select {
case <-first:
t.Fatal("duplicate refresh was not coalesced")
default:
}
select {
case <-second:
t.Fatal("organization B received organization A refresh")
default:
}
removeFirst()
if _, removeReplacement, err := hub.subscribe("organization-a"); err != nil {
t.Fatalf("released capacity was not reusable: %v", err)
} else {
removeReplacement()
}
}
+810
View File
@@ -0,0 +1,810 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"bytes"
"compress/gzip"
"crypto/ecdh"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"strings"
"time"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/model"
"gamertan.com/observatory/internal/nativeprotocol"
"gamertan.com/observatory/internal/otlp"
"gamertan.com/observatory/internal/query"
"gamertan.com/observatory/internal/storage"
"gamertan.com/web/access"
"gamertan.com/web/auth"
"gamertan.com/web/authhttp"
"gamertan.com/web/requestmeta"
"gamertan.com/web/websec"
)
type Options struct {
PublicOrigin string
MaxBodyBytes int64
MaxConcurrentIngest int
MaxQueryRows int
QueryBudget query.Budget
SessionLifetime time.Duration
PushPublicKey string
PushDispatcher PushDispatcher
}
type PushDispatcher interface {
Enqueue(organizationID string) bool
}
type Server struct {
store *storage.Store
identity *identity.Services
options Options
cookie authhttp.CookieConfig
now func() time.Time
refresh *refreshHub
requests *requestmeta.Resolver
ingestSlots chan struct{}
}
func New(store *storage.Store, identities *identity.Services, options Options) (*Server, error) {
if store == nil || identities == nil || identities.Auth == nil || identities.Access == nil {
return nil, errors.New("server storage and identity services are required")
}
origin, err := url.Parse(options.PublicOrigin)
if err != nil || origin.Scheme != "https" || origin.Host == "" || origin.User != nil || origin.Path != "" && origin.Path != "/" || origin.RawQuery != "" || origin.Fragment != "" {
return nil, errors.New("server public origin must be an absolute HTTPS origin")
}
options.PublicOrigin = strings.TrimSuffix(options.PublicOrigin, "/")
if options.MaxConcurrentIngest == 0 {
options.MaxConcurrentIngest = 8
}
if options.MaxBodyBytes < 1024 || options.MaxConcurrentIngest < 1 || options.MaxConcurrentIngest > 64 || options.MaxQueryRows < 1 || options.SessionLifetime < 5*time.Minute || options.SessionLifetime > 30*24*time.Hour {
return nil, errors.New("server limits are invalid")
}
if options.QueryBudget.MaxRows != options.MaxQueryRows || options.QueryBudget.MaxDuration < time.Millisecond || options.QueryBudget.MaxScannedBytes < 1 || options.QueryBudget.MaxMemoryBytes < 1 {
return nil, errors.New("server query budget is invalid")
}
if (options.PushPublicKey == "") != (options.PushDispatcher == nil) {
return nil, errors.New("server Web Push key and dispatcher must be configured together")
}
if options.PushPublicKey != "" {
publicKey, decodeErr := base64.RawURLEncoding.DecodeString(options.PushPublicKey)
if decodeErr != nil || len(publicKey) != 65 {
return nil, errors.New("server Web Push public key is invalid")
}
if _, decodeErr = ecdh.P256().NewPublicKey(publicKey); decodeErr != nil {
return nil, errors.New("server Web Push public key is invalid")
}
}
cookie := authhttp.CookieConfig{Name: "__Host-observatory_session", Lifetime: options.SessionLifetime, SameSite: http.SameSiteStrictMode}
if err = cookie.Validate(); err != nil {
return nil, err
}
requests, err := requestmeta.New(requestmeta.Config{})
if err != nil {
return nil, fmt.Errorf("server request metadata: %w", err)
}
return &Server{store: store, identity: identities, options: options, cookie: cookie, now: func() time.Time { return time.Now().UTC() }, refresh: newRefreshHub(256, 8), requests: requests, ingestSlots: make(chan struct{}, options.MaxConcurrentIngest)}, nil
}
func (s *Server) enterIngest(w http.ResponseWriter) (func(), bool) {
select {
case s.ingestSlots <- struct{}{}:
return func() { <-s.ingestSlots }, true
default:
w.Header().Set("Retry-After", "1")
writeProblem(w, http.StatusServiceUnavailable, "ingestion capacity temporarily unavailable")
return nil, false
}
}
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", textOK("ok\n"))
mux.HandleFunc("HEAD /healthz", textOK("ok\n"))
mux.HandleFunc("GET /readyz", textOK("ready\n"))
mux.HandleFunc("HEAD /readyz", textOK("ready\n"))
mux.HandleFunc("GET /{$}", s.landing)
mux.HandleFunc("HEAD /{$}", s.landing)
mux.HandleFunc("GET /manifest.webmanifest", s.webManifest)
mux.HandleFunc("HEAD /manifest.webmanifest", s.webManifest)
mux.HandleFunc("GET /service-worker.js", s.serviceWorker)
mux.HandleFunc("HEAD /service-worker.js", s.serviceWorker)
mux.HandleFunc("GET /offline/{$}", s.offlineShell)
mux.HandleFunc("HEAD /offline/{$}", s.offlineShell)
mux.HandleFunc("GET /login/{$}", s.loginPage)
mux.HandleFunc("HEAD /login/{$}", s.loginPage)
mux.HandleFunc("POST /login/{$}", s.loginForm)
mux.HandleFunc("POST /logout/{$}", s.logoutForm)
mux.HandleFunc("GET /account/password/{$}", s.passwordPage)
mux.HandleFunc("HEAD /account/password/{$}", s.passwordPage)
mux.HandleFunc("POST /account/password/{$}", s.passwordForm)
mux.HandleFunc("GET /app/{$}", s.app)
mux.HandleFunc("HEAD /app/{$}", s.app)
mux.HandleFunc("GET /app/explore/{$}", s.explorePage)
mux.HandleFunc("HEAD /app/explore/{$}", s.explorePage)
mux.HandleFunc("POST /app/explore/{$}", s.exploreForm)
mux.HandleFunc("GET /app/events", s.events)
mux.HandleFunc("POST /app/queries/{$}", s.createSavedQuery)
mux.HandleFunc("POST /app/queries/builder/{$}", s.createBuiltQuery)
mux.HandleFunc("POST /app/dashboards/{$}", s.createDashboard)
mux.HandleFunc("GET /app/dashboards/{slug}/{$}", s.dashboard)
mux.HandleFunc("HEAD /app/dashboards/{slug}/{$}", s.dashboard)
mux.HandleFunc("POST /app/dashboards/{slug}/{$}", s.updateDashboard)
mux.HandleFunc("POST /app/dashboards/{slug}/panels/{$}", s.addDashboardPanel)
mux.HandleFunc("POST /app/dashboards/{slug}/panels/{panel}/{$}", s.updateDashboardPanel)
mux.HandleFunc("POST /app/dashboards/{slug}/panels/{panel}/remove/{$}", s.removeDashboardPanel)
mux.HandleFunc("GET /app/dashboards/{slug}/export.json", s.exportDashboard)
mux.HandleFunc("HEAD /app/dashboards/{slug}/export.json", s.exportDashboard)
mux.HandleFunc("GET /app/incidents/{$}", s.incidentInbox)
mux.HandleFunc("HEAD /app/incidents/{$}", s.incidentInbox)
mux.HandleFunc("GET /app/incidents/offline/{$}", s.offlineIncidentInbox)
mux.HandleFunc("HEAD /app/incidents/offline/{$}", s.offlineIncidentInbox)
mux.HandleFunc("POST /app/alert-rules/{$}", s.createAlertRule)
mux.HandleFunc("POST /app/incidents/{id}/{$}", s.transitionIncident)
mux.HandleFunc("GET /assets/", s.serveAsset)
mux.HandleFunc("HEAD /assets/", s.serveAsset)
mux.HandleFunc("POST /api/v1/ingest/native", s.ingest)
mux.HandleFunc("POST /api/v2/ingest/native", s.ingestFramed)
mux.HandleFunc("POST /v1/logs", s.ingestOTLP(otlp.Logs))
mux.HandleFunc("POST /v1/metrics", s.ingestOTLP(otlp.Metrics))
mux.HandleFunc("POST /v1/traces", s.ingestOTLP(otlp.Traces))
mux.HandleFunc("POST /api/v1/agent/enroll", s.enrollAgent)
mux.HandleFunc("POST /api/v1/agent/alert-transition", s.recordAgentAlertTransition)
mux.HandleFunc("DELETE /api/v1/agent/source", s.revokeAgentSource)
mux.HandleFunc("POST /api/v1/session", s.login)
mux.HandleFunc("DELETE /api/v1/session", s.logout)
mux.HandleFunc("POST /api/v1/account/password", s.changePassword)
mux.HandleFunc("POST /api/v1/query/parse", s.parseQuery)
mux.HandleFunc("POST /api/v1/query/explain", s.explainQuery)
mux.HandleFunc("POST /api/v1/query", s.executeQuery)
mux.HandleFunc("POST /api/v1/push/subscription", s.savePushSubscription)
mux.HandleFunc("POST /api/v1/push/subscription/status", s.pushSubscriptionStatus)
mux.HandleFunc("DELETE /api/v1/push/subscription", s.deletePushSubscription)
return securityHeaders(s.requests.Middleware(authhttp.Optional(s.identity.Auth, s.cookie)(s.requirePasswordChange(mux))))
}
func (s *Server) recordAgentAlertTransition(w http.ResponseWriter, r *http.Request) {
token, ok := bearer(r.Header.Get("Authorization"))
if !ok || !strings.HasPrefix(token, "obs1.") {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
// Authenticate before decoding. RecordSourceAlertTransition authenticates
// again while binding scope and raw evidence to the credential.
if _, err := s.store.Authenticate(r.Context(), token); err != nil {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
leave, ok := s.enterIngest(w)
if !ok {
return
}
defer leave()
body := http.MaxBytesReader(w, r.Body, 64<<10)
defer body.Close()
var transition model.AlertTransition
if err := decodeOne(body, &transition); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid source alert transition")
return
}
ack, err := s.store.RecordSourceAlertTransition(r.Context(), token, transition, s.now())
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "source alert transition rejected")
return
}
writeJSON(w, http.StatusAccepted, ack)
}
func (s *Server) requirePasswordChange(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok || !principal.User.PasswordChangeRequired || passwordChangeAllowed(r) {
next.ServeHTTP(w, r)
return
}
if r.Method == http.MethodGet || r.Method == http.MethodHead {
http.Redirect(w, r, "/account/password/", http.StatusSeeOther)
return
}
writeProblem(w, http.StatusForbidden, "password change required")
})
}
func passwordChangeAllowed(r *http.Request) bool {
switch r.URL.Path {
case "/healthz", "/readyz", "/manifest.webmanifest", "/service-worker.js", "/offline/", "/account/password/", "/logout/", "/api/v1/account/password", "/api/v1/session":
return true
}
return strings.HasPrefix(r.URL.Path, "/assets/")
}
func (s *Server) revokeAgentSource(w http.ResponseWriter, r *http.Request) {
token, ok := bearer(r.Header.Get("Authorization"))
if !ok || !strings.HasPrefix(token, "obs1.") {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
source, err := s.store.Authenticate(r.Context(), token)
if err != nil {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
if err = s.store.RevokeSource(r.Context(), source.ID); err != nil {
writeProblem(w, http.StatusServiceUnavailable, "source revocation unavailable")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) enrollAgent(w http.ResponseWriter, r *http.Request) {
token, ok := bearer(r.Header.Get("Authorization"))
if !ok || !strings.HasPrefix(token, "obse1.") {
writeProblem(w, http.StatusUnauthorized, "valid enrollment required")
return
}
body := http.MaxBytesReader(w, r.Body, 1)
defer body.Close()
payload, bodyErr := io.ReadAll(body)
if bodyErr != nil || len(payload) != 0 {
writeProblem(w, http.StatusBadRequest, "enrollment request body must be empty")
return
}
enrollment, credential, err := s.store.RedeemEnrollment(r.Context(), token, s.now())
if err != nil {
writeProblem(w, http.StatusUnauthorized, "valid enrollment required")
return
}
writeJSON(w, http.StatusCreated, struct {
SourceID string `json:"source_id"`
Credential string `json:"credential"`
}{enrollment.SourceID, credential})
}
func (s *Server) ingest(w http.ResponseWriter, r *http.Request) {
token, ok := bearer(r.Header.Get("Authorization"))
if !ok {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
source, err := s.store.Authenticate(r.Context(), token)
if err != nil {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
leave, ok := s.enterIngest(w)
if !ok {
return
}
defer leave()
body := http.MaxBytesReader(w, r.Body, s.options.MaxBodyBytes)
defer body.Close()
var batch model.Batch
if err := decodeOne(body, &batch); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid native batch")
return
}
ack, err := s.store.Ingest(r.Context(), token, batch, s.now())
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "batch rejected")
return
}
s.refresh.publish(source.Scope.OrganizationID)
writeJSON(w, http.StatusAccepted, ack)
}
func (s *Server) ingestFramed(w http.ResponseWriter, r *http.Request) {
token, ok := bearer(r.Header.Get("Authorization"))
if !ok {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
source, err := s.store.Authenticate(r.Context(), token)
if err != nil {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
leave, ok := s.enterIngest(w)
if !ok {
return
}
defer leave()
mediaType, parameters, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || mediaType != "application/json" || len(parameters) != 0 {
writeProblem(w, http.StatusUnsupportedMediaType, "native JSON content type required")
return
}
envelope, err := nativeprotocol.ParseHeaders(r.Header, s.options.MaxBodyBytes)
if err != nil {
writeProblem(w, http.StatusBadRequest, "invalid native batch envelope")
return
}
body := http.MaxBytesReader(w, r.Body, s.options.MaxBodyBytes)
defer body.Close()
if _, exact, checkErr := s.store.CheckNativeReplay(r.Context(), token, envelope); checkErr != nil {
writeProblem(w, http.StatusUnprocessableEntity, "batch rejected")
return
} else if exact {
if err = verifyNativeReplayBody(body, envelope); err != nil {
writeProblem(w, http.StatusBadRequest, "native batch body does not match envelope")
return
}
ack, confirmErr := s.store.ConfirmNativeReplay(r.Context(), token, envelope)
if confirmErr != nil {
writeProblem(w, http.StatusUnprocessableEntity, "batch rejected")
return
}
writeJSON(w, http.StatusAccepted, ack)
return
}
encoded, err := io.ReadAll(body)
if err != nil {
writeProblem(w, http.StatusBadRequest, "invalid native batch")
return
}
var batch model.Batch
if err = decodeOne(bytes.NewReader(encoded), &batch); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid native batch")
return
}
ack, err := s.store.IngestNative(r.Context(), token, batch, envelope, encoded, s.now())
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "batch rejected")
return
}
if !ack.Duplicate {
s.refresh.publish(source.Scope.OrganizationID)
}
writeJSON(w, http.StatusAccepted, ack)
}
func verifyNativeReplayBody(body io.Reader, envelope model.BatchEnvelope) error {
digest := sha256.New()
written, err := io.Copy(digest, body)
if err != nil || written != envelope.EncodedBytes || hex.EncodeToString(digest.Sum(nil)) != envelope.WireDigest {
return errors.New("native batch body does not match envelope")
}
return nil
}
func (s *Server) ingestOTLP(signal otlp.Signal) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token, ok := bearer(r.Header.Get("Authorization"))
if !ok || !strings.HasPrefix(token, "obs1.") {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
// Authenticate before parsing attacker-controlled protobuf. IngestAuto
// authenticates again while assigning the next sequence under its source
// lock so revocation cannot race into an acknowledged write.
source, err := s.store.Authenticate(r.Context(), token)
if err != nil {
writeProblem(w, http.StatusUnauthorized, "source authentication required")
return
}
leave, ok := s.enterIngest(w)
if !ok {
return
}
defer leave()
mediaType, parameters, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || mediaType != "application/x-protobuf" || len(parameters) != 0 {
writeProblem(w, http.StatusUnsupportedMediaType, "OTLP protobuf content type required")
return
}
body, status, err := readOTLPBody(w, r, s.options.MaxBodyBytes)
if err != nil {
title := "invalid OTLP request body"
if status == http.StatusRequestEntityTooLarge {
title = "OTLP request body exceeds limit"
} else if status == http.StatusUnsupportedMediaType {
title = "unsupported OTLP content encoding"
}
writeProblem(w, status, title)
return
}
now := s.now()
records, err := otlp.Decode(signal, body, now)
if err != nil {
writeProblem(w, http.StatusBadRequest, "invalid OTLP protobuf payload")
return
}
if _, err = s.store.IngestAuto(r.Context(), token, signal.StreamID(), signal.ModelSignal(), records, now); err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "OTLP batch rejected")
return
}
s.refresh.publish(source.Scope.OrganizationID)
response, err := otlp.SuccessResponse(signal)
if err != nil {
writeProblem(w, http.StatusInternalServerError, "OTLP response unavailable")
return
}
w.Header().Set("Content-Type", "application/x-protobuf")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(response)))
w.WriteHeader(http.StatusOK)
if len(response) > 0 {
_, _ = w.Write(response)
}
}
}
func readOTLPBody(w http.ResponseWriter, r *http.Request, limit int64) ([]byte, int, error) {
compressed := http.MaxBytesReader(w, r.Body, limit)
defer compressed.Close()
encoding := strings.TrimSpace(strings.ToLower(r.Header.Get("Content-Encoding")))
var reader io.Reader = compressed
var zipped *gzip.Reader
switch encoding {
case "", "identity":
case "gzip":
var err error
zipped, err = gzip.NewReader(compressed)
if err != nil {
return nil, http.StatusBadRequest, err
}
defer zipped.Close()
reader = io.LimitReader(zipped, limit+1)
default:
return nil, http.StatusUnsupportedMediaType, errors.New("unsupported content encoding")
}
body, err := io.ReadAll(reader)
if err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
return nil, http.StatusRequestEntityTooLarge, err
}
return nil, http.StatusBadRequest, err
}
if int64(len(body)) > limit {
return nil, http.StatusRequestEntityTooLarge, errors.New("decoded body exceeds limit")
}
return body, http.StatusOK, nil
}
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
if !websec.SameOrigin(r, s.options.PublicOrigin) {
writeProblem(w, http.StatusForbidden, "same-origin request required")
return
}
body := http.MaxBytesReader(w, r.Body, 16<<10)
defer body.Close()
var input struct {
Identifier string `json:"identifier"`
Password string `json:"password"`
}
if err := decodeOne(body, &input); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid session request")
return
}
token, principal, err := s.identity.Auth.Authenticate(r.Context(), input.Identifier, input.Password, s.options.SessionLifetime)
if err != nil {
writeProblem(w, http.StatusUnauthorized, "invalid credentials")
return
}
if err = authhttp.SetSession(w, s.cookie, token, s.now()); err != nil {
_ = s.identity.Auth.RevokeSession(r.Context(), token)
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
csrf, err := authhttp.CSRFToken(token, "session:delete")
if err != nil {
_ = s.identity.Auth.RevokeSession(r.Context(), token)
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
passwordCSRF := ""
if principal.User.PasswordChangeRequired {
passwordCSRF, err = authhttp.CSRFToken(token, "account:password:change")
if err != nil {
_ = s.identity.Auth.RevokeSession(r.Context(), token)
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
}
writeJSON(w, http.StatusOK, struct {
UserID string `json:"user_id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
CSRFToken string `json:"csrf_token"`
PasswordChangeCSRF string `json:"password_change_csrf,omitempty"`
PasswordChangeRequired bool `json:"password_change_required"`
}{principal.User.ID, principal.User.Username, principal.User.DisplayName, csrf, passwordCSRF, principal.User.PasswordChangeRequired})
}
func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
if !websec.SameOrigin(r, s.options.PublicOrigin) {
writeProblem(w, http.StatusForbidden, "same-origin request required")
return
}
principal, ok := auth.PrincipalFromContext(r.Context())
token, tokenOK := authhttp.SessionToken(r, s.cookie)
if !ok || !tokenOK || !principal.User.PasswordChangeRequired || !authhttp.VerifyCSRF(token, "account:password:change", r.Header.Get("X-CSRF-Token")) {
writeProblem(w, http.StatusForbidden, "password change authorization required")
return
}
body := http.MaxBytesReader(w, r.Body, 8<<10)
defer body.Close()
var input struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
if err := decodeOne(body, &input); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid password change request")
return
}
if err := s.identity.Auth.ChangePassword(r.Context(), principal.User.ID, input.CurrentPassword, input.NewPassword); err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "password change rejected")
return
}
if err := authhttp.ClearSession(w, s.cookie); err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
if !websec.SameOrigin(r, s.options.PublicOrigin) {
writeProblem(w, http.StatusForbidden, "same-origin request required")
return
}
token, ok := authhttp.SessionToken(r, s.cookie)
if !ok || !authhttp.VerifyCSRF(token, "session:delete", r.Header.Get("X-CSRF-Token")) {
writeProblem(w, http.StatusForbidden, "valid session CSRF token required")
return
}
if err := s.identity.Auth.RevokeSession(r.Context(), token); err != nil && !errors.Is(err, auth.ErrSessionNotFound) {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
if err := authhttp.ClearSession(w, s.cookie); err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
w.WriteHeader(http.StatusNoContent)
}
type queryRequest struct {
Query string `json:"query,omitempty"`
AST *query.AST `json:"ast,omitempty"`
OrganizationID string `json:"organization_id,omitempty"`
ProjectID string `json:"project_id,omitempty"`
EnvironmentID string `json:"environment_id,omitempty"`
ServiceID string `json:"service_id,omitempty"`
}
func (s *Server) parseQuery(w http.ResponseWriter, r *http.Request) {
body := http.MaxBytesReader(w, r.Body, 16<<10)
defer body.Close()
var input queryRequest
if err := decodeOne(body, &input); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid query request")
return
}
ast, err := parseAST(input, s.options.MaxQueryRows)
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "query rejected")
return
}
writeJSON(w, http.StatusOK, ast)
}
func (s *Server) explainQuery(w http.ResponseWriter, r *http.Request) {
if !websec.SameOrigin(r, s.options.PublicOrigin) {
writeProblem(w, http.StatusForbidden, "same-origin request required")
return
}
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return
}
body := http.MaxBytesReader(w, r.Body, 16<<10)
defer body.Close()
var input queryRequest
if err := decodeOne(body, &input); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid query request")
return
}
ast, err := parseAST(input, s.options.MaxQueryRows)
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "query rejected")
return
}
requested := access.Scope{OrganizationID: input.OrganizationID, ProjectID: input.ProjectID, EnvironmentID: input.EnvironmentID, ServiceID: input.ServiceID}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, requested, identity.PermissionTelemetryQuery)
if err != nil {
writeProblem(w, http.StatusBadRequest, "invalid resource scope")
return
}
if !decision.Allowed {
writeProblem(w, http.StatusForbidden, "resource access denied")
return
}
if err := s.identity.ValidateResourceScope(r.Context(), requested); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid resource scope")
return
}
sensitive, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, requested, identity.PermissionTelemetryReadSensitive)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "authorization unavailable")
return
}
estimated, err := s.store.EstimateOrganizationBytes(input.OrganizationID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "query planning unavailable")
return
}
registry, _, err := s.store.ActiveDescriptors(r.Context(), input.OrganizationID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "query planning unavailable")
return
}
explain, err := query.Plan(ast, query.Scope{
OrganizationID: input.OrganizationID, ProjectID: input.ProjectID,
EnvironmentID: input.EnvironmentID, ServiceID: input.ServiceID,
Sensitive: sensitive.Allowed,
}, registry, estimated, s.options.QueryBudget)
if errors.Is(err, query.ErrSensitivePermissionRequired) {
writeProblem(w, http.StatusForbidden, "sensitive-field permission required")
return
}
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "query plan rejected")
return
}
writeJSON(w, http.StatusOK, explain)
}
func (s *Server) executeQuery(w http.ResponseWriter, r *http.Request) {
if !websec.SameOrigin(r, s.options.PublicOrigin) {
writeProblem(w, http.StatusForbidden, "same-origin request required")
return
}
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return
}
body := http.MaxBytesReader(w, r.Body, 16<<10)
defer body.Close()
var input queryRequest
if err := decodeOne(body, &input); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid query request")
return
}
ast, err := parseAST(input, s.options.MaxQueryRows)
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "query rejected")
return
}
requested := access.Scope{OrganizationID: input.OrganizationID, ProjectID: input.ProjectID, EnvironmentID: input.EnvironmentID, ServiceID: input.ServiceID}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, requested, identity.PermissionTelemetryQuery)
if err != nil {
writeProblem(w, http.StatusBadRequest, "invalid resource scope")
return
}
if !decision.Allowed {
writeProblem(w, http.StatusForbidden, "resource access denied")
return
}
if err = s.identity.ValidateResourceScope(r.Context(), requested); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid resource scope")
return
}
sensitive, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, requested, identity.PermissionTelemetryReadSensitive)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "authorization unavailable")
return
}
result, err := s.store.Query(r.Context(), ast, query.Scope{
OrganizationID: input.OrganizationID, ProjectID: input.ProjectID,
EnvironmentID: input.EnvironmentID, ServiceID: input.ServiceID,
Sensitive: sensitive.Allowed,
}, s.options.QueryBudget, s.now())
switch {
case errors.Is(err, query.ErrSensitivePermissionRequired):
writeProblem(w, http.StatusForbidden, "sensitive-field permission required")
return
case errors.Is(err, query.ErrBudgetExceeded):
writeProblem(w, http.StatusUnprocessableEntity, "query execution budget exceeded")
return
case errors.Is(err, query.ErrTypeMismatch):
writeProblem(w, http.StatusUnprocessableEntity, "query field type mismatch")
return
case err != nil:
writeProblem(w, http.StatusServiceUnavailable, "query execution unavailable")
return
}
writeJSON(w, http.StatusOK, result)
}
func parseAST(input queryRequest, maxRows int) (query.AST, error) {
if (input.Query == "") == (input.AST == nil) {
return query.AST{}, errors.New("provide exactly one query or AST")
}
if input.AST == nil {
return query.Parse(input.Query, maxRows)
}
ast := *input.AST
var err error
if ast.WindowText != "" {
ast.Window, err = time.ParseDuration(ast.WindowText)
}
if err == nil && ast.BucketText != "" {
ast.Bucket, err = time.ParseDuration(ast.BucketText)
}
if err == nil {
err = query.Validate(ast, maxRows)
}
return ast, err
}
func decodeOne(reader io.Reader, value any) error {
decoder := json.NewDecoder(reader)
decoder.DisallowUnknownFields()
if err := decoder.Decode(value); err != nil {
return err
}
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return errors.New("request must contain exactly one JSON value")
}
return nil
}
func bearer(value string) (string, bool) {
const prefix = "Bearer "
if !strings.HasPrefix(value, prefix) || strings.ContainsAny(value[len(prefix):], " \t\r\n") {
return "", false
}
return value[len(prefix):], value[len(prefix):] != ""
}
func textOK(body string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
if r.Method != http.MethodHead {
_, _ = io.WriteString(w, body)
}
}
}
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self'; manifest-src 'self'; script-src 'self'; style-src 'self'; worker-src 'self'")
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("Permissions-Policy", "camera=(), geolocation=(), microphone=()")
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(w, r)
})
}
func writeJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}
func writeProblem(w http.ResponseWriter, status int, title string) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(struct {
Title string `json:"title"`
Status int `json:"status"`
}{Title: title, Status: status})
}
+496
View File
@@ -0,0 +1,496 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"bytes"
"compress/gzip"
"context"
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/model"
"gamertan.com/observatory/internal/nativeprotocol"
"gamertan.com/observatory/internal/query"
"gamertan.com/observatory/internal/schema"
"gamertan.com/observatory/internal/storage"
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
logspb "go.opentelemetry.io/proto/otlp/logs/v1"
"google.golang.org/protobuf/proto"
)
func TestIngestionConcurrencyIsBoundedAndReusable(t *testing.T) {
server, store, identities, _ := newUITestServer(t)
defer store.Close()
defer identities.Close()
server.ingestSlots = make(chan struct{}, 1)
first := httptest.NewRecorder()
leave, ok := server.enterIngest(first)
if !ok || leave == nil {
t.Fatal("first ingestion slot was unavailable")
}
second := httptest.NewRecorder()
if secondLeave, admitted := server.enterIngest(second); admitted || secondLeave != nil || second.Code != http.StatusServiceUnavailable || second.Header().Get("Retry-After") != "1" || !strings.Contains(second.Body.String(), "ingestion capacity temporarily unavailable") {
t.Fatalf("second admission admitted=%t status=%d retry=%q body=%s", admitted, second.Code, second.Header().Get("Retry-After"), second.Body.String())
}
leave()
third := httptest.NewRecorder()
thirdLeave, admitted := server.enterIngest(third)
if !admitted || thirdLeave == nil {
t.Fatal("released ingestion capacity was not reusable")
}
thirdLeave()
}
func TestServerWebPushConfigurationIsAllOrNothing(t *testing.T) {
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
defer store.Close()
identities, err := identity.Open(root)
if err != nil {
t.Fatal(err)
}
defer identities.Close()
dispatcher := &recordingPushDispatcher{}
options := testOptions()
options.PushDispatcher = dispatcher
if _, err = New(store, identities, options); err == nil {
t.Fatal("dispatcher without public key accepted")
}
options.PushPublicKey = "invalid"
if _, err = New(store, identities, options); err == nil {
t.Fatal("invalid public key accepted")
}
private, err := ecdh.P256().GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
options.PushPublicKey = base64.RawURLEncoding.EncodeToString(private.PublicKey().Bytes())
if _, err = New(store, identities, options); err != nil {
t.Fatalf("valid Web Push configuration: %v", err)
}
}
func TestIngestDoesNotExposeValuesInErrors(t *testing.T) {
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
defer store.Close()
token, err := store.CreateSource(context.Background(), "source", model.Scope{OrganizationID: "org", ProjectID: "p", EnvironmentID: "prod", ServiceID: "s"})
if err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
identities, err := identity.Open(root)
if err != nil {
t.Fatal(err)
}
defer identities.Close()
server, err := New(store, identities, testOptions())
if err != nil {
t.Fatal(err)
}
server.now = func() time.Time { return now }
batch := model.Batch{Version: 1, SourceID: "source", StreamID: "logs", Sequence: 1, ObservedAt: now, Signal: model.SignalLogs, Records: []model.Observation{{Timestamp: now, Name: "request", Body: "credential=do-not-echo"}}}
body, _ := json.Marshal(batch)
req := httptest.NewRequest(http.MethodPost, "/api/v1/ingest/native", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
server.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusAccepted {
t.Fatalf("expected 202, got %d: %s", rec.Code, rec.Body.String())
}
req = httptest.NewRequest(http.MethodPost, "/api/v1/ingest/native", bytes.NewBufferString(`{"secret":"do-not-echo"}`))
req.Header.Set("Authorization", "Bearer "+token)
rec = httptest.NewRecorder()
server.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest || bytes.Contains(rec.Body.Bytes(), []byte("do-not-echo")) {
t.Fatalf("unsafe error response %d: %s", rec.Code, rec.Body.String())
}
}
func TestFramedNativeIngestAcknowledgesExactReplayAndOverlappingTime(t *testing.T) {
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
defer store.Close()
token, err := store.CreateSource(context.Background(), "source", model.Scope{OrganizationID: "org", ProjectID: "p", EnvironmentID: "prod", ServiceID: "s"})
if err != nil {
t.Fatal(err)
}
identities, err := identity.Open(root)
if err != nil {
t.Fatal(err)
}
defer identities.Close()
now := time.Date(2026, 8, 18, 20, 0, 0, 0, time.UTC)
server, err := New(store, identities, testOptions())
if err != nil {
t.Fatal(err)
}
server.now = func() time.Time { return now }
handler := server.Handler()
send := func(batch model.Batch, body []byte, envelope model.BatchEnvelope) *httptest.ResponseRecorder {
t.Helper()
request := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v2/ingest/native", bytes.NewReader(body))
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("Content-Type", "application/json")
nativeprotocol.SetHeaders(request.Header, envelope)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
return response
}
batch := model.Batch{Version: model.BatchVersion, SourceID: "source", StreamID: "logs", Sequence: 1, ObservedAt: now, Signal: model.SignalLogs, Records: []model.Observation{{Timestamp: now, Name: "first"}}}
body, _ := json.Marshal(batch)
envelope, _ := batch.Envelope(body)
first := send(batch, body, envelope)
if first.Code != http.StatusAccepted || strings.Contains(first.Body.String(), `"duplicate":true`) {
t.Fatalf("first status=%d body=%s", first.Code, first.Body.String())
}
replay := send(batch, body, envelope)
if replay.Code != http.StatusAccepted || !strings.Contains(replay.Body.String(), `"duplicate":true`) {
t.Fatalf("replay status=%d body=%s", replay.Code, replay.Body.String())
}
tampered := append(append([]byte(nil), body...), ' ')
bad := send(batch, tampered, envelope)
if bad.Code != http.StatusBadRequest {
t.Fatalf("tampered status=%d body=%s", bad.Code, bad.Body.String())
}
batch.Sequence = 2
batch.ObservedAt = now.Add(time.Second)
// The second batch intentionally overlaps the first batch's observed time.
body, _ = json.Marshal(batch)
envelope, _ = batch.Envelope(body)
overlap := send(batch, body, envelope)
if overlap.Code != http.StatusAccepted {
t.Fatalf("overlap status=%d body=%s", overlap.Code, overlap.Body.String())
}
}
func TestOTLPHTTPIngestionIsAuthenticatedBoundedAndCompressed(t *testing.T) {
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
defer store.Close()
scope := model.Scope{OrganizationID: "organization-a", ProjectID: "project-a", EnvironmentID: "production", ServiceID: "service-a"}
token, err := store.CreateSource(context.Background(), "otlp-source", scope)
if err != nil {
t.Fatal(err)
}
identities, err := identity.Open(root)
if err != nil {
t.Fatal(err)
}
defer identities.Close()
now := time.Date(2026, 8, 17, 3, 0, 0, 0, time.UTC)
server, err := New(store, identities, testOptions())
if err != nil {
t.Fatal(err)
}
server.now = func() time.Time { return now }
handler := server.Handler()
payload, err := proto.Marshal(&logspb.LogsData{ResourceLogs: []*logspb.ResourceLogs{{ScopeLogs: []*logspb.ScopeLogs{{LogRecords: []*logspb.LogRecord{{
TimeUnixNano: uint64(now.UnixNano()), EventName: "http.request", Body: &commonpb.AnyValue{Value: &commonpb.AnyValue_StringValue{StringValue: "accepted"}},
}}}}}}})
if err != nil {
t.Fatal(err)
}
send := func(body []byte, encoding string) *httptest.ResponseRecorder {
t.Helper()
request := httptest.NewRequest(http.MethodPost, "https://observatory.example/v1/logs", bytes.NewReader(body))
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("Content-Type", "application/x-protobuf")
if encoding != "" {
request.Header.Set("Content-Encoding", encoding)
}
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
return response
}
plain := send(payload, "")
if plain.Code != http.StatusOK || plain.Header().Get("Content-Type") != "application/x-protobuf" || plain.Header().Get("Content-Length") != "0" || plain.Body.Len() != 0 {
t.Fatalf("plain status=%d headers=%v body=%q", plain.Code, plain.Header(), plain.Body.String())
}
var compressed bytes.Buffer
zipper := gzip.NewWriter(&compressed)
if _, err = zipper.Write(payload); err != nil {
t.Fatal(err)
}
if err = zipper.Close(); err != nil {
t.Fatal(err)
}
if response := send(compressed.Bytes(), "gzip"); response.Code != http.StatusOK {
t.Fatalf("gzip status=%d body=%s", response.Code, response.Body.String())
}
if err = store.Recover(context.Background()); err != nil {
t.Fatal(err)
}
if bytes, err := store.EstimateOrganizationBytes(scope.OrganizationID); err != nil || bytes == 0 {
t.Fatalf("projection bytes=%d err=%v", bytes, err)
}
unauthorized := httptest.NewRequest(http.MethodPost, "https://observatory.example/v1/logs", bytes.NewReader(payload))
unauthorized.Header.Set("Content-Type", "application/x-protobuf")
unauthorizedResult := httptest.NewRecorder()
handler.ServeHTTP(unauthorizedResult, unauthorized)
if unauthorizedResult.Code != http.StatusUnauthorized {
t.Fatalf("unauthorized status=%d", unauthorizedResult.Code)
}
wrongMedia := httptest.NewRequest(http.MethodPost, "https://observatory.example/v1/logs", bytes.NewReader(payload))
wrongMedia.Header.Set("Authorization", "Bearer "+token)
wrongMedia.Header.Set("Content-Type", "application/json")
wrongMediaResult := httptest.NewRecorder()
handler.ServeHTTP(wrongMediaResult, wrongMedia)
if wrongMediaResult.Code != http.StatusUnsupportedMediaType {
t.Fatalf("media status=%d", wrongMediaResult.Code)
}
compressed.Reset()
zipper = gzip.NewWriter(&compressed)
if _, err = zipper.Write(bytes.Repeat([]byte{'x'}, int(testOptions().MaxBodyBytes)+1)); err != nil {
t.Fatal(err)
}
if err = zipper.Close(); err != nil {
t.Fatal(err)
}
tooLarge := send(compressed.Bytes(), "gzip")
if tooLarge.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("oversized status=%d body=%s", tooLarge.Code, tooLarge.Body.String())
}
}
func TestSessionAndScopedExplain(t *testing.T) {
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
defer store.Close()
identities, err := identity.Open(root)
if err != nil {
t.Fatal(err)
}
defer identities.Close()
bootstrap, err := identities.Bootstrap(context.Background(), identity.BootstrapInput{Username: "operator", Email: "operator@example.test", DisplayName: "Operator", Password: "correct horse battery staple"})
if err != nil {
t.Fatal(err)
}
server, err := New(store, identities, testOptions())
if err != nil {
t.Fatal(err)
}
handler := server.Handler()
login := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v1/session", strings.NewReader(`{"identifier":"operator","password":"correct horse battery staple"}`))
login.Header.Set("Origin", "https://observatory.example")
loginResult := httptest.NewRecorder()
handler.ServeHTTP(loginResult, login)
if loginResult.Code != http.StatusOK || strings.Contains(loginResult.Body.String(), "correct horse") {
t.Fatalf("login status=%d body=%s", loginResult.Code, loginResult.Body.String())
}
var session struct {
CSRFToken string `json:"csrf_token"`
}
if err = json.Unmarshal(loginResult.Body.Bytes(), &session); err != nil || session.CSRFToken == "" {
t.Fatalf("session=%+v err=%v", session, err)
}
cookies := loginResult.Result().Cookies()
if len(cookies) != 1 || cookies[0].Name != "__Host-observatory_session" || !cookies[0].Secure || !cookies[0].HttpOnly {
t.Fatalf("cookies=%+v", cookies)
}
explainBody := fmt.Sprintf(`{"organization_id":%q,"query":"logs | where status >= 500 | limit 10"}`, bootstrap.Organization.ID)
explain := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v1/query/explain", strings.NewReader(explainBody))
explain.Header.Set("Origin", "https://observatory.example")
explain.AddCookie(cookies[0])
explainResult := httptest.NewRecorder()
handler.ServeHTTP(explainResult, explain)
if explainResult.Code != http.StatusOK || !strings.Contains(explainResult.Body.String(), `"projected_sources"`) {
t.Fatalf("explain status=%d body=%s", explainResult.Code, explainResult.Body.String())
}
now := time.Now().UTC()
sourceToken, err := store.CreateSource(context.Background(), "query-source", model.Scope{OrganizationID: bootstrap.Organization.ID, ProjectID: "project-a", EnvironmentID: "production", ServiceID: "service-a"})
if err != nil {
t.Fatal(err)
}
batch := model.Batch{Version: model.BatchVersion, SourceID: "query-source", StreamID: "requests", Sequence: 1, ObservedAt: now, Signal: model.SignalLogs, Records: []model.Observation{{Timestamp: now, Name: "application.http.request", Attributes: map[string]string{"http.status_code": "503", "http.route": "/failed", "workshop.queue_depth": "12"}}}}
if _, err = store.Ingest(context.Background(), sourceToken, batch, now); err != nil {
t.Fatal(err)
}
if err = store.Recover(context.Background()); err != nil {
t.Fatal(err)
}
execute := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v1/query", strings.NewReader(explainBody))
execute.Header.Set("Origin", "https://observatory.example")
execute.AddCookie(cookies[0])
executeResult := httptest.NewRecorder()
handler.ServeHTTP(executeResult, execute)
if executeResult.Code != http.StatusOK || !strings.Contains(executeResult.Body.String(), `"http.status_code"`) || !strings.Contains(executeResult.Body.String(), `"503"`) {
t.Fatalf("execute status=%d body=%s", executeResult.Code, executeResult.Body.String())
}
reviewed := schema.Descriptor{Version: schema.DescriptorVersion, Signal: model.SignalLogs, Field: "workshop.queue_depth", Type: schema.TypeInteger, Meaning: "Reviewed queue depth for one application service.", Sensitivity: schema.SensitivityInternal, Cardinality: schema.CardinalityLow, Index: schema.IndexRange, Retention: schema.RetentionRaw, ProjectionVersion: 1}
if _, err = store.ActivateDescriptor(context.Background(), bootstrap.Organization.ID, reviewed, now.Add(time.Second)); err != nil {
t.Fatal(err)
}
customBody := fmt.Sprintf(`{"organization_id":%q,"query":"logs | where workshop.queue_depth >= 10 | limit 10"}`, bootstrap.Organization.ID)
for _, path := range []string{"/api/v1/query/explain", "/api/v1/query"} {
request := httptest.NewRequest(http.MethodPost, "https://observatory.example"+path, strings.NewReader(customBody))
request.Header.Set("Origin", "https://observatory.example")
request.AddCookie(cookies[0])
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"workshop.queue_depth"`) || !strings.Contains(response.Body.String(), `"indexed":true`) {
t.Fatalf("path=%s status=%d body=%s", path, response.Code, response.Body.String())
}
}
for _, path := range []string{"/api/v1/query/explain", "/api/v1/query"} {
denied := httptest.NewRequest(http.MethodPost, "https://observatory.example"+path, strings.NewReader(`{"organization_id":"unowned1","query":"logs | limit 10"}`))
denied.Header.Set("Origin", "https://observatory.example")
denied.AddCookie(cookies[0])
deniedResult := httptest.NewRecorder()
handler.ServeHTTP(deniedResult, denied)
if deniedResult.Code != http.StatusForbidden {
t.Fatalf("path=%s cross-organization status=%d body=%s", path, deniedResult.Code, deniedResult.Body.String())
}
}
logout := httptest.NewRequest(http.MethodDelete, "https://observatory.example/api/v1/session", nil)
logout.Header.Set("Origin", "https://observatory.example")
logout.Header.Set("X-CSRF-Token", session.CSRFToken)
logout.AddCookie(cookies[0])
logoutResult := httptest.NewRecorder()
handler.ServeHTTP(logoutResult, logout)
if logoutResult.Code != http.StatusNoContent {
t.Fatalf("logout status=%d body=%s", logoutResult.Code, logoutResult.Body.String())
}
}
func TestCrossOriginLoginIsRejected(t *testing.T) {
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
defer store.Close()
identities, err := identity.Open(root)
if err != nil {
t.Fatal(err)
}
defer identities.Close()
server, err := New(store, identities, testOptions())
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v1/session", strings.NewReader(`{"identifier":"operator","password":"secret"}`))
request.Header.Set("Origin", "https://attacker.example")
response := httptest.NewRecorder()
server.Handler().ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
func TestAgentEnrollmentIsSingleUseAndCredentialCanSelfRevoke(t *testing.T) {
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
defer store.Close()
identities, err := identity.Open(root)
if err != nil {
t.Fatal(err)
}
defer identities.Close()
now := time.Date(2026, 8, 17, 2, 0, 0, 0, time.UTC)
enrollmentToken, _, err := store.CreateEnrollment(context.Background(), "source-a", model.Scope{OrganizationID: "organization-a", ProjectID: "project-a", EnvironmentID: "production", ServiceID: "service-a"}, "operator-a", 15*time.Minute, now)
if err != nil {
t.Fatal(err)
}
server, err := New(store, identities, testOptions())
if err != nil {
t.Fatal(err)
}
server.now = func() time.Time { return now.Add(time.Minute) }
enroll := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v1/agent/enroll", nil)
enroll.Header.Set("Authorization", "Bearer "+enrollmentToken)
response := httptest.NewRecorder()
server.Handler().ServeHTTP(response, enroll)
if response.Code != http.StatusCreated || strings.Contains(response.Body.String(), enrollmentToken) {
t.Fatalf("enroll status=%d body=%s", response.Code, response.Body.String())
}
var enrolled struct {
SourceID string `json:"source_id"`
Credential string `json:"credential"`
}
if err = json.Unmarshal(response.Body.Bytes(), &enrolled); err != nil || enrolled.SourceID != "source-a" || enrolled.Credential == "" {
t.Fatalf("enrolled=%+v err=%v", enrolled, err)
}
replay := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v1/agent/enroll", nil)
replay.Header.Set("Authorization", "Bearer "+enrollmentToken)
replayResponse := httptest.NewRecorder()
server.Handler().ServeHTTP(replayResponse, replay)
if replayResponse.Code != http.StatusUnauthorized {
t.Fatalf("replay status=%d", replayResponse.Code)
}
revoke := httptest.NewRequest(http.MethodDelete, "https://observatory.example/api/v1/agent/source", nil)
revoke.Header.Set("Authorization", "Bearer "+enrolled.Credential)
revokeResponse := httptest.NewRecorder()
server.Handler().ServeHTTP(revokeResponse, revoke)
if revokeResponse.Code != http.StatusNoContent {
t.Fatalf("revoke status=%d body=%s", revokeResponse.Code, revokeResponse.Body.String())
}
if _, err = store.Authenticate(context.Background(), enrolled.Credential); err == nil {
t.Fatal("revoked source credential remained active")
}
}
func testOptions() Options {
return Options{PublicOrigin: "https://observatory.example", MaxBodyBytes: 1 << 20, MaxQueryRows: 1000, SessionLifetime: time.Hour, QueryBudget: query.Budget{MaxDuration: 2 * time.Second, MaxRows: 1000, MaxScannedBytes: 10 << 20, MaxMemoryBytes: 8 << 20}}
}
+92
View File
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/model"
"gamertan.com/observatory/internal/storage"
)
func TestAgentAlertTransitionIsAuthenticatedBoundedAndEvidenceBacked(t *testing.T) {
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
defer store.Close()
identities, err := identity.Open(root)
if err != nil {
t.Fatal(err)
}
defer identities.Close()
now := time.Date(2026, 8, 18, 23, 45, 0, 0, time.UTC)
scope := model.Scope{OrganizationID: "organization-a", ProjectID: "project-a", EnvironmentID: "production", ServiceID: "service-a"}
token, err := store.CreateSource(context.Background(), "source-a", scope)
if err != nil {
t.Fatal(err)
}
saved, err := store.SaveQuery(context.Background(), storage.SavedQueryInput{OrganizationID: scope.OrganizationID, ActorUserID: "operator-a", MaxRows: 100, Name: "Logs", Query: "logs | limit 10", Scope: storage.ResourceScope{ProjectID: scope.ProjectID, EnvironmentID: scope.EnvironmentID, ServiceID: scope.ServiceID}}, now)
if err != nil {
t.Fatal(err)
}
rule, err := store.SaveAlertRule(context.Background(), storage.AlertRuleInput{OrganizationID: scope.OrganizationID, ActorUserID: "operator-a", SavedQueryID: saved.ID, Name: "Logs", Severity: "warning", MinimumMatches: 1, RequiredConsecutive: 1, EvaluationInterval: 15 * time.Second, Enabled: true}, now)
if err != nil {
t.Fatal(err)
}
batch := model.Batch{Version: model.BatchVersion, SourceID: "source-a", StreamID: "requests", Sequence: 1, ObservedAt: now, Signal: model.SignalLogs, Records: []model.Observation{{Timestamp: now, Name: "request"}}}
ingested, err := store.Ingest(context.Background(), token, batch, now)
if err != nil {
t.Fatal(err)
}
server, err := New(store, identities, testOptions())
if err != nil {
t.Fatal(err)
}
server.now = func() time.Time { return now }
handler := server.Handler()
transition := model.AlertTransition{Version: model.AlertTransitionVersion, RuleID: rule.ID, RuleRevision: rule.Revision, AgentEpoch: strings.Repeat("a", 32), Sequence: 1, StreamID: batch.StreamID, BatchSequence: batch.Sequence, SegmentDigest: ingested.Digest, WindowStart: now, WindowEnd: now, State: "matched", ObservedAt: now}
payload, err := json.Marshal(transition)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v1/agent/alert-transition", bytes.NewReader(payload))
request.Header.Set("Authorization", "Bearer "+token)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusAccepted {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
var ack storage.SourceAlertTransitionAck
if err = json.Unmarshal(response.Body.Bytes(), &ack); err != nil || ack.SourceID != "source-a" || ack.RuleID != rule.ID || ack.Duplicate {
t.Fatalf("ack=%+v err=%v", ack, err)
}
unauthorized := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v1/agent/alert-transition", bytes.NewReader([]byte(`{"state":"do-not-echo"}`)))
unauthorizedResult := httptest.NewRecorder()
handler.ServeHTTP(unauthorizedResult, unauthorized)
if unauthorizedResult.Code != http.StatusUnauthorized || strings.Contains(unauthorizedResult.Body.String(), "do-not-echo") {
t.Fatalf("unauthorized status=%d body=%q", unauthorizedResult.Code, unauthorizedResult.Body.String())
}
oversized := httptest.NewRequest(http.MethodPost, "https://observatory.example/api/v1/agent/alert-transition", strings.NewReader(strings.Repeat("x", (64<<10)+1)))
oversized.Header.Set("Authorization", "Bearer "+token)
oversizedResult := httptest.NewRecorder()
handler.ServeHTTP(oversizedResult, oversized)
if oversizedResult.Code != http.StatusBadRequest {
t.Fatalf("oversized status=%d body=%q", oversizedResult.Code, oversizedResult.Body.String())
}
}
+532
View File
@@ -0,0 +1,532 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"bytes"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"strings"
"time"
"unicode/utf8"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/model"
"gamertan.com/observatory/internal/query"
"gamertan.com/observatory/internal/site"
"gamertan.com/sandwich-hime/sando"
"gamertan.com/web/access"
"gamertan.com/web/auth"
"gamertan.com/web/authhttp"
"gamertan.com/web/websec"
)
const loginCSRFCookieName = "__Host-observatory_login_csrf"
const (
loginFormFailure = "This sign-in form expired or could not be verified. Please try again."
loginCredentialFailure = "The username or password was not accepted."
passwordFormFailure = "This password form expired or could not be verified. Please try again."
passwordMatchFailure = "The new passwords did not match. Please enter them again."
passwordChangeFailure = "The password could not be changed. Check the temporary password and choose a different password of at least 12 characters."
)
func (s *Server) landing(w http.ResponseWriter, r *http.Request) {
if _, ok := auth.PrincipalFromContext(r.Context()); ok {
http.Redirect(w, r, "/app/", http.StatusSeeOther)
return
}
view := site.LandingView{Head: s.head("Gamertan Observatory", "A self-hosted observability platform in development for carefully operated Linux systems.", "/")}
s.renderHTML(w, r, http.StatusOK, site.Landing(view))
}
func (s *Server) loginPage(w http.ResponseWriter, r *http.Request) {
if principal, ok := auth.PrincipalFromContext(r.Context()); ok {
if principal.User.PasswordChangeRequired {
http.Redirect(w, r, "/account/password/", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/app/", http.StatusSeeOther)
return
}
s.renderLogin(w, r, http.StatusOK, "")
}
func (s *Server) loginForm(w http.ResponseWriter, r *http.Request) {
values, err := readFormFields(w, r, 16<<10, []string{"identifier", "password"}, []string{"csrf_token"})
csrfOK := err == nil && validLoginCSRF(r, values.Get("csrf_token"))
if !tokenBoundFormRequest(r, s.options.PublicOrigin, csrfOK) {
s.renderLogin(w, r, http.StatusForbidden, loginFormFailure)
return
}
if err != nil {
s.renderLogin(w, r, http.StatusBadRequest, loginFormFailure)
return
}
token, principal, err := s.identity.Auth.Authenticate(r.Context(), values.Get("identifier"), values.Get("password"), s.options.SessionLifetime)
if err != nil {
s.renderLogin(w, r, http.StatusUnauthorized, loginCredentialFailure)
return
}
if err = authhttp.SetSession(w, s.cookie, token, s.now()); err != nil {
_ = s.identity.Auth.RevokeSession(r.Context(), token)
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
if _, cookieErr := r.Cookie(loginCSRFCookieName); cookieErr == nil {
clearLoginCSRF(w)
}
location := "/app/"
if principal.User.PasswordChangeRequired {
location = "/account/password/"
}
http.Redirect(w, r, location, http.StatusSeeOther)
}
// tokenBoundFormRequest makes the purpose-bound form token the primary CSRF
// proof. Browser origin metadata is defense in depth: an explicit cross-site
// or contradictory origin still fails closed, while absent or opaque metadata
// does not break an otherwise valid ordinary HTML form submission.
func tokenBoundFormRequest(r *http.Request, publicOrigin string, validToken bool) bool {
if !validToken {
return false
}
fetchSite := strings.ToLower(strings.TrimSpace(r.Header.Get("Sec-Fetch-Site")))
if fetchSite != "" && fetchSite != "same-origin" && fetchSite != "none" {
return false
}
origin := strings.TrimSpace(r.Header.Get("Origin"))
if origin == "" || origin == "null" {
return true
}
return websec.SameOrigin(r, publicOrigin)
}
func (s *Server) renderLogin(w http.ResponseWriter, r *http.Request, status int, message string) {
csrf, err := s.issueLoginCSRF(w)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "login unavailable")
return
}
view := site.LoginView{Head: s.head("Sign in — Gamertan Observatory", "Sign in to the local Gamertan Observatory workshop.", "/login/"), CSRFToken: csrf, ErrorMessage: message}
s.renderHTML(w, r, status, site.Login(view))
}
func (s *Server) issueLoginCSRF(w http.ResponseWriter) (string, error) {
secret := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, secret); err != nil {
return "", errors.New("generate login CSRF token")
}
token := base64.RawURLEncoding.EncodeToString(secret)
http.SetCookie(w, &http.Cookie{Name: loginCSRFCookieName, Value: token, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, Expires: s.now().Add(10 * time.Minute), MaxAge: 600})
return token, nil
}
func validLoginCSRF(r *http.Request, candidate string) bool {
cookie, err := r.Cookie(loginCSRFCookieName)
if err != nil {
return false
}
want, wantErr := base64.RawURLEncoding.DecodeString(cookie.Value)
got, gotErr := base64.RawURLEncoding.DecodeString(candidate)
return wantErr == nil && gotErr == nil && len(want) == 32 && len(got) == len(want) && subtle.ConstantTimeCompare(want, got) == 1
}
func clearLoginCSRF(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{Name: loginCSRFCookieName, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, MaxAge: -1})
}
func (s *Server) passwordPage(w http.ResponseWriter, r *http.Request) {
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok {
http.Redirect(w, r, "/login/", http.StatusSeeOther)
return
}
if !principal.User.PasswordChangeRequired {
http.Redirect(w, r, "/app/", http.StatusSeeOther)
return
}
s.renderPassword(w, r, http.StatusOK, "")
}
func (s *Server) passwordForm(w http.ResponseWriter, r *http.Request) {
principal, ok := auth.PrincipalFromContext(r.Context())
token, tokenOK := authhttp.SessionToken(r, s.cookie)
values, err := readForm(w, r, 8<<10, "csrf_token", "current_password", "new_password", "confirm_password")
csrfOK := err == nil && tokenOK && authhttp.VerifyCSRF(token, "account:password:change", values.Get("csrf_token"))
if !tokenBoundFormRequest(r, s.options.PublicOrigin, csrfOK) {
if ok && tokenOK && principal.User.PasswordChangeRequired {
s.renderPassword(w, r, http.StatusForbidden, passwordFormFailure)
} else {
http.Redirect(w, r, "/login/", http.StatusSeeOther)
}
return
}
if !ok || !tokenOK || !principal.User.PasswordChangeRequired {
writeProblem(w, http.StatusForbidden, "password change authorization required")
return
}
if err != nil || !csrfOK {
s.renderPassword(w, r, http.StatusBadRequest, passwordFormFailure)
return
}
if values.Get("new_password") != values.Get("confirm_password") {
s.renderPassword(w, r, http.StatusUnprocessableEntity, passwordMatchFailure)
return
}
if err = s.identity.Auth.ChangePassword(r.Context(), principal.User.ID, values.Get("current_password"), values.Get("new_password")); err != nil {
s.renderPassword(w, r, http.StatusUnprocessableEntity, passwordChangeFailure)
return
}
if err = authhttp.ClearSession(w, s.cookie); err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
http.Redirect(w, r, "/login/?password=changed", http.StatusSeeOther)
}
func (s *Server) renderPassword(w http.ResponseWriter, r *http.Request, status int, message string) {
token, ok := authhttp.SessionToken(r, s.cookie)
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return
}
csrf, err := authhttp.CSRFToken(token, "account:password:change")
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
view := site.PasswordView{Head: s.head("Choose your password — Gamertan Observatory", "Replace the one-time Observatory credential before continuing.", "/account/password/"), CSRFToken: csrf, ErrorMessage: message}
s.renderHTML(w, r, status, site.Password(view))
}
func (s *Server) logoutForm(w http.ResponseWriter, r *http.Request) {
values, err := readForm(w, r, 4<<10, "csrf_token")
token, ok := authhttp.SessionToken(r, s.cookie)
csrfOK := err == nil && ok && authhttp.VerifyCSRF(token, "session:delete", values.Get("csrf_token"))
if !tokenBoundFormRequest(r, s.options.PublicOrigin, csrfOK) {
writeProblem(w, http.StatusForbidden, "valid sign-out form required")
return
}
if err != nil {
writeProblem(w, http.StatusBadRequest, "invalid sign-out request")
return
}
if !csrfOK {
writeProblem(w, http.StatusForbidden, "valid session CSRF token required")
return
}
if err = s.identity.Auth.RevokeSession(r.Context(), token); err != nil && !errors.Is(err, auth.ErrSessionNotFound) {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
if err = authhttp.ClearSession(w, s.cookie); err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
http.Redirect(w, r, "/login/", http.StatusSeeOther)
}
func (s *Server) app(w http.ResponseWriter, r *http.Request) {
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok {
http.Redirect(w, r, "/login/", http.StatusSeeOther)
return
}
organizations, err := s.identity.OrganizationsForUser(r.Context(), principal.User.ID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "organization list unavailable")
return
}
if len(organizations) == 0 {
writeProblem(w, http.StatusForbidden, "organization access required")
return
}
queryValues := r.URL.Query()
requested := queryValues.Get("organization")
if len(queryValues) > 0 {
selectedValues, exists := queryValues["organization"]
if !exists || len(queryValues) != 1 || len(selectedValues) != 1 || selectedValues[0] == "" {
writeProblem(w, http.StatusBadRequest, "invalid organization selection")
return
}
}
selected := organizations[0]
if requested != "" {
found := false
for _, organization := range organizations {
if organization.ID == requested {
selected, found = organization, true
break
}
}
if !found {
writeProblem(w, http.StatusForbidden, "organization access denied")
return
}
}
scope := access.Scope{OrganizationID: selected.ID}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionDashboardsRead)
if err != nil || !decision.Allowed {
writeProblem(w, http.StatusForbidden, "dashboard access denied")
return
}
token, ok := authhttp.SessionToken(r, s.cookie)
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return
}
csrf, err := authhttp.CSRFToken(token, "session:delete")
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
view := site.AppView{
Head: s.head("Overview — Gamertan Observatory", "Recent authorized telemetry and saved Observatory work.", "/app/"),
DisplayName: principal.User.DisplayName, CSRFToken: csrf,
EventsURL: "/app/events?organization=" + url.QueryEscape(selected.ID),
RefreshedAt: s.now().Format("2006-01-02 15:04:05 UTC"),
Organization: site.OrganizationOption{ID: selected.ID, Name: selected.Name, Selected: true},
IncidentsURL: "/app/incidents/?organization=" + url.QueryEscape(selected.ID),
}
projectionStatus, projectionErr := s.store.OrganizationProjectionStatus(r.Context(), selected.ID, s.now())
if projectionErr == nil && projectionStatus.PendingSegments > 0 {
view.PendingBatches = projectionStatus.PendingSegments
view.ProjectionLag = formatProjectionLag(projectionStatus.OldestPendingLag)
}
incidentDecision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionIncidentsRead)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "authorization unavailable")
return
}
if incidentDecision.Allowed {
incidents, incidentErr := s.store.Incidents(r.Context(), selected.ID, false, 100)
if incidentErr != nil {
writeProblem(w, http.StatusServiceUnavailable, "incidents unavailable")
return
}
view.OpenIncidents = len(incidents)
}
manage, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionDashboardsManage)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "authorization unavailable")
return
}
view.CanManage = manage.Allowed
if view.CanManage {
view.ManageCSRF, err = authhttp.CSRFToken(token, "dashboards:manage")
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "session unavailable")
return
}
}
for _, organization := range organizations {
view.Organizations = append(view.Organizations, site.OrganizationOption{ID: organization.ID, Name: organization.Name, Selected: organization.ID == selected.ID})
}
view.Signals = s.overviewSignals(r, selected.ID)
saved, err := s.store.SavedQueries(r.Context(), selected.ID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "saved queries unavailable")
return
}
for _, item := range saved {
view.SavedQueries = append(view.SavedQueries, site.SavedQuerySummary{ID: item.ID, Name: item.Name, Description: item.Description, Query: item.Query})
}
dashboards, err := s.store.Dashboards(r.Context(), selected.ID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "dashboards unavailable")
return
}
for _, item := range dashboards {
view.Dashboards = append(view.Dashboards, site.DashboardSummary{Slug: item.Slug, Name: item.Name, Description: item.Description, PanelCount: len(item.Panels)})
}
s.renderHTML(w, r, http.StatusOK, site.App(view))
}
func formatProjectionLag(lag time.Duration) string {
if lag < time.Second {
return "less than one second"
}
return lag.Round(time.Second).String()
}
func (s *Server) overviewSignals(r *http.Request, organizationID string) []site.SignalView {
definitions := []struct {
signal model.Signal
id, name string
description string
}{
{model.SignalLogs, "logs", "logs", "Recent structured events accepted for this organization."},
{model.SignalMetrics, "metrics", "metrics", "Recent numeric observations with a table alternative."},
{model.SignalTraces, "traces", "traces", "Recent spans and their correlation identities."},
{model.SignalDeployments, "deployments", "deployments", "Recent bounded deployment evidence."},
}
views := make([]site.SignalView, 0, len(definitions))
for _, definition := range definitions {
text := string(definition.signal) + " | window 1h | limit 20"
ast, err := query.Parse(text, s.options.MaxQueryRows)
var result query.Result
if err == nil {
result, err = s.store.Query(r.Context(), ast, query.Scope{OrganizationID: organizationID}, s.options.QueryBudget, s.now())
}
table := site.TableView{Caption: "Recent " + definition.name, Columns: []site.TableColumn{{Label: "Status"}}, Empty: "No observations are available in the last hour."}
if err != nil {
table.Empty = "This bounded query is temporarily unavailable."
} else {
table = resultTable("Recent "+definition.name, result)
}
views = append(views, site.SignalView{ID: definition.id, Name: definition.name, Description: definition.description, Query: text, Table: table})
}
return views
}
func resultTable(caption string, result query.Result) site.TableView {
table := site.TableView{Caption: caption, Empty: "No observations are available in the last hour."}
for _, column := range result.Columns {
table.Columns = append(table.Columns, site.TableColumn{Label: column.Field, Unit: column.Unit})
}
for _, row := range result.Rows {
view := site.TableRow{Values: make([]string, len(result.Columns))}
for index := range view.Values {
view.Values[index] = "—"
if index < len(row.Values) && row.Values[index] != nil {
view.Values[index] = boundedCell(*row.Values[index])
}
}
table.Rows = append(table.Rows, view)
}
return table
}
func boundedCell(value string) string {
const maxRunes = 256
if utf8.RuneCountInString(value) <= maxRunes {
return value
}
runes := []rune(value)
return string(runes[:maxRunes]) + "…"
}
func (s *Server) events(w http.ResponseWriter, r *http.Request) {
principal, ok := auth.PrincipalFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "authentication required")
return
}
organizationID := r.URL.Query().Get("organization")
if len(r.URL.Query()) != 1 || len(r.URL.Query()["organization"]) != 1 {
writeProblem(w, http.StatusBadRequest, "organization is required")
return
}
scope := access.Scope{OrganizationID: organizationID}
if err := s.identity.ValidateResourceScope(r.Context(), scope); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid organization")
return
}
decision, err := s.identity.Access.Authorize(r.Context(), principal.User.ID, scope, identity.PermissionDashboardsRead)
if err != nil || !decision.Allowed {
writeProblem(w, http.StatusForbidden, "dashboard access denied")
return
}
updates, remove, err := s.refresh.subscribe(organizationID)
if err != nil {
writeProblem(w, http.StatusServiceUnavailable, "live refresh capacity reached")
return
}
defer remove()
flusher, ok := w.(http.Flusher)
if !ok {
writeProblem(w, http.StatusNotImplemented, "streaming unavailable")
return
}
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Accel-Buffering", "no")
_, _ = io.WriteString(w, "event: ready\ndata: {}\n\n")
flusher.Flush()
heartbeat := time.NewTicker(20 * time.Second)
defer heartbeat.Stop()
for {
select {
case <-r.Context().Done():
return
case <-updates:
_, _ = io.WriteString(w, "event: refresh\ndata: {}\n\n")
flusher.Flush()
case <-heartbeat.C:
_, _ = io.WriteString(w, ": keepalive\n\n")
flusher.Flush()
}
}
}
func (s *Server) serveAsset(w http.ResponseWriter, r *http.Request) {
body, contentType, ok := site.Asset(r.URL.Path)
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
if r.Method != http.MethodHead {
_, _ = w.Write(body)
}
}
func (s *Server) head(title, description, path string) site.HeadView {
return site.HeadView{Title: title, Description: description, CanonicalURL: s.options.PublicOrigin + path, Assets: site.AssetPaths()}
}
func (s *Server) renderHTML(w http.ResponseWriter, r *http.Request, status int, component sando.Component) {
var body bytes.Buffer
if err := sando.Render(r.Context(), &body, component); err != nil {
writeProblem(w, http.StatusInternalServerError, "interface render unavailable")
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Length", fmt.Sprintf("%d", body.Len()))
w.WriteHeader(status)
if r.Method != http.MethodHead {
_, _ = w.Write(body.Bytes())
}
}
func readForm(w http.ResponseWriter, r *http.Request, limit int64, fields ...string) (url.Values, error) {
return readFormFields(w, r, limit, fields, nil)
}
func readFormFields(w http.ResponseWriter, r *http.Request, limit int64, required, optional []string) (url.Values, error) {
mediaType, parameters, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || mediaType != "application/x-www-form-urlencoded" || len(parameters) > 1 || len(parameters) == 1 && !strings.EqualFold(parameters["charset"], "utf-8") {
return nil, errors.New("URL-encoded form required")
}
body := http.MaxBytesReader(w, r.Body, limit)
defer body.Close()
encoded, err := io.ReadAll(body)
if err != nil || !utf8.Valid(encoded) {
return nil, errors.New("invalid form body")
}
values, err := url.ParseQuery(string(encoded))
if err != nil || len(values) != len(required)+len(optional) {
return nil, errors.New("invalid form fields")
}
for _, field := range required {
if len(values[field]) != 1 || values.Get(field) == "" {
return nil, errors.New("invalid form field")
}
}
for _, field := range optional {
if len(values[field]) != 1 {
return nil, errors.New("invalid form field")
}
}
return values, nil
}
+1122
View File
@@ -0,0 +1,1122 @@
// SPDX-License-Identifier: AGPL-3.0-only
package httpserver
import (
"bytes"
"context"
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"html"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"time"
"gamertan.com/observatory/internal/identity"
"gamertan.com/observatory/internal/model"
"gamertan.com/observatory/internal/query"
"gamertan.com/observatory/internal/schema"
"gamertan.com/observatory/internal/site"
"gamertan.com/observatory/internal/storage"
"gamertan.com/web/authhttp"
)
func TestHandlerAssignsFreshBoundedRequestIDs(t *testing.T) {
server, store, identities, _ := newUITestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
var previous string
for _, test := range []struct {
method string
target string
}{
{http.MethodGet, "https://observatory.example/healthz"},
{http.MethodHead, "https://observatory.example/readyz"},
{http.MethodGet, "https://observatory.example/"},
{http.MethodGet, "https://observatory.example/not-found"},
} {
header := http.Header{"X-Request-ID": []string{"attacker-selected"}}
response := perform(handler, test.method, test.target, nil, nil, header)
requestID := response.Header().Get("X-Request-ID")
decoded, err := hex.DecodeString(requestID)
if err != nil || len(decoded) != 16 || requestID == "attacker-selected" || requestID == previous {
t.Fatalf("%s %s request_id=%q decoded=%d err=%v previous=%q", test.method, test.target, requestID, len(decoded), err, previous)
}
previous = requestID
}
}
func TestHTMLInterfaceAuthenticationAssetsAndOverview(t *testing.T) {
server, store, identities, bootstrap := newUITestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
landing := perform(handler, http.MethodGet, "https://observatory.example/", nil, nil)
if landing.Code != http.StatusOK || landing.Header().Get("Content-Type") != "text/html; charset=utf-8" || !strings.Contains(landing.Body.String(), "Keep the evidence close.") {
t.Fatalf("landing status=%d body=%s", landing.Code, landing.Body.String())
}
csp := landing.Header().Get("Content-Security-Policy")
if strings.Contains(csp, "unsafe-inline") || !strings.Contains(csp, "script-src 'self'") || !strings.Contains(csp, "style-src 'self'") || !strings.Contains(csp, "manifest-src 'self'") || !strings.Contains(csp, "worker-src 'self'") || strings.Contains(landing.Body.String(), "<style") {
t.Fatalf("CSP=%q body=%s", csp, landing.Body.String())
}
if !strings.Contains(landing.Body.String(), `<link rel="manifest" href="/manifest.webmanifest">`) {
t.Fatalf("landing omitted manifest discovery: %s", landing.Body.String())
}
head := perform(handler, http.MethodHead, "https://observatory.example/", nil, nil)
if head.Code != http.StatusOK || head.Body.Len() != 0 || head.Header().Get("Content-Length") != landing.Header().Get("Content-Length") {
t.Fatalf("HEAD status=%d length=%q body=%d", head.Code, head.Header().Get("Content-Length"), head.Body.Len())
}
for _, path := range []string{site.AssetPaths().StylePath, site.AssetPaths().ScriptPath, site.AssetPaths().IconPath} {
asset := perform(handler, http.MethodGet, "https://observatory.example"+path, nil, nil)
if asset.Code != http.StatusOK || asset.Body.Len() == 0 || asset.Header().Get("Cache-Control") != "public, max-age=31536000, immutable" {
t.Fatalf("asset %s status=%d cache=%q", path, asset.Code, asset.Header().Get("Cache-Control"))
}
assetHead := perform(handler, http.MethodHead, "https://observatory.example"+path, nil, nil)
if assetHead.Code != http.StatusOK || assetHead.Body.Len() != 0 || assetHead.Header().Get("Content-Length") != asset.Header().Get("Content-Length") {
t.Fatalf("asset HEAD %s status=%d", path, assetHead.Code)
}
}
manifest := perform(handler, http.MethodGet, "https://observatory.example/manifest.webmanifest", nil, nil)
if manifest.Code != http.StatusOK || manifest.Header().Get("Content-Type") != "application/manifest+json" || !strings.Contains(manifest.Body.String(), site.AssetPaths().IconPath) {
t.Fatalf("manifest status=%d headers=%v body=%s", manifest.Code, manifest.Header(), manifest.Body.String())
}
manifestHead := perform(handler, http.MethodHead, "https://observatory.example/manifest.webmanifest", nil, nil)
if manifestHead.Code != http.StatusOK || manifestHead.Body.Len() != 0 || manifestHead.Header().Get("Content-Length") != manifest.Header().Get("Content-Length") {
t.Fatalf("manifest HEAD status=%d body=%d", manifestHead.Code, manifestHead.Body.Len())
}
worker := perform(handler, http.MethodGet, "https://observatory.example/service-worker.js", nil, nil)
if worker.Code != http.StatusOK || worker.Header().Get("Content-Type") != "text/javascript; charset=utf-8" || worker.Header().Get("Cache-Control") != "no-cache" || worker.Header().Get("Service-Worker-Allowed") != "/" || !strings.Contains(worker.Body.String(), "cache-inbox") {
t.Fatalf("worker status=%d headers=%v", worker.Code, worker.Header())
}
offline := perform(handler, http.MethodGet, "https://observatory.example/offline/", nil, nil)
if offline.Code != http.StatusOK || !strings.Contains(offline.Body.String(), "The evidence is still safe.") {
t.Fatalf("offline status=%d body=%s", offline.Code, offline.Body.String())
}
disabledPush := perform(handler, http.MethodPost, "https://observatory.example/api/v1/push/subscription", strings.NewReader(`{}`), nil)
if disabledPush.Code != http.StatusNotFound {
t.Fatalf("disabled push status=%d body=%s", disabledPush.Code, disabledPush.Body.String())
}
unauthenticated := perform(handler, http.MethodGet, "https://observatory.example/app/", nil, nil)
if unauthenticated.Code != http.StatusSeeOther || unauthenticated.Header().Get("Location") != "/login/" {
t.Fatalf("unauthenticated status=%d location=%q", unauthenticated.Code, unauthenticated.Header().Get("Location"))
}
extraQuery := perform(handler, http.MethodGet, "https://observatory.example/app/?unexpected=true", nil, []*http.Cookie{loginHTML(t, handler)})
if extraQuery.Code != http.StatusBadRequest {
t.Fatalf("unexpected query status=%d", extraQuery.Code)
}
loginCookie := loginHTML(t, handler)
now := server.now()
token, err := store.CreateSource(context.Background(), "ui-source", model.Scope{OrganizationID: bootstrap.Organization.ID, ProjectID: "project-a", EnvironmentID: "production", ServiceID: "service-a"})
if err != nil {
t.Fatal(err)
}
batch := model.Batch{Version: model.BatchVersion, SourceID: "ui-source", StreamID: "requests", Sequence: 1, ObservedAt: now, Signal: model.SignalLogs, Records: []model.Observation{{Timestamp: now, Name: "application.http.request", Attributes: map[string]string{"http.status_code": "200", "http.route": "/"}}}}
body, _ := json.Marshal(batch)
updates, remove, err := server.refresh.subscribe(bootstrap.Organization.ID)
if err != nil {
t.Fatal(err)
}
defer remove()
ingestHeaders := http.Header{"Authorization": []string{"Bearer " + token}, "Content-Type": []string{"application/json"}}
ingest := perform(handler, http.MethodPost, "https://observatory.example/api/v1/ingest/native", bytes.NewReader(body), nil, ingestHeaders)
if ingest.Code != http.StatusAccepted {
t.Fatalf("ingest status=%d body=%s", ingest.Code, ingest.Body.String())
}
pending := perform(handler, http.MethodGet, "https://observatory.example/app/", nil, []*http.Cookie{loginCookie})
if pending.Code != http.StatusOK || !strings.Contains(pending.Body.String(), "Durable evidence is still being indexed.") || !strings.Contains(pending.Body.String(), "Accepted batches safely stored: 1.") || !strings.Contains(pending.Body.String(), `role="status"`) {
t.Fatalf("pending app status=%d body=%s", pending.Code, pending.Body.String())
}
if err = store.Recover(context.Background()); err != nil {
t.Fatal(err)
}
select {
case <-updates:
default:
t.Fatal("successful ingest did not publish organization refresh")
}
app := perform(handler, http.MethodGet, "https://observatory.example/app/", nil, []*http.Cookie{loginCookie})
if app.Code != http.StatusOK || !strings.Contains(app.Body.String(), bootstrap.Organization.Name) || !strings.Contains(app.Body.String(), "application.http.request") || !strings.Contains(app.Body.String(), "Recent metrics") || !strings.Contains(app.Body.String(), "Recent traces") || !strings.Contains(app.Body.String(), "Recent deployments") || !strings.Contains(app.Body.String(), `action="/app/queries/builder/"`) || !strings.Contains(app.Body.String(), "Build a query") {
t.Fatalf("app status=%d body=%s", app.Code, app.Body.String())
}
if strings.Contains(app.Body.String(), "Durable evidence is still being indexed.") {
t.Fatalf("app retained indexing status after projection completed: %s", app.Body.String())
}
for _, forbidden := range []string{"Render #", "request number", "position:sticky", "unsafe-inline"} {
if strings.Contains(app.Body.String(), forbidden) {
t.Fatalf("app exposed forbidden marker %q", forbidden)
}
}
appHead := perform(handler, http.MethodHead, "https://observatory.example/app/", nil, []*http.Cookie{loginCookie})
if appHead.Code != http.StatusOK || appHead.Body.Len() != 0 || appHead.Header().Get("Content-Length") != app.Header().Get("Content-Length") {
t.Fatalf("app HEAD status=%d body=%d", appHead.Code, appHead.Body.Len())
}
sessionToken := loginCookie.Value
csrf, err := authhttp.CSRFToken(sessionToken, "session:delete")
if err != nil {
t.Fatal(err)
}
logout := url.Values{"csrf_token": []string{csrf}}.Encode()
logoutHeaders := http.Header{"Origin": []string{"null"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
loggedOut := perform(handler, http.MethodPost, "https://observatory.example/logout/", strings.NewReader(logout), []*http.Cookie{loginCookie}, logoutHeaders)
if loggedOut.Code != http.StatusSeeOther || loggedOut.Header().Get("Location") != "/login/" {
t.Fatalf("logout status=%d location=%q", loggedOut.Code, loggedOut.Header().Get("Location"))
}
}
func TestExploreWorkbenchUsesBoundedServerRenderedQueries(t *testing.T) {
server, store, identities, bootstrap := newUITestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
unauthenticated := perform(handler, http.MethodGet, "https://observatory.example/app/explore/?organization="+url.QueryEscape(bootstrap.Organization.ID), nil, nil)
if unauthenticated.Code != http.StatusSeeOther || unauthenticated.Header().Get("Location") != "/login/" {
t.Fatalf("unauthenticated status=%d location=%q", unauthenticated.Code, unauthenticated.Header().Get("Location"))
}
cookie := loginHTML(t, handler)
missingOrganization := perform(handler, http.MethodGet, "https://observatory.example/app/explore/", nil, []*http.Cookie{cookie})
if missingOrganization.Code != http.StatusBadRequest {
t.Fatalf("missing organization status=%d body=%s", missingOrganization.Code, missingOrganization.Body.String())
}
target := "https://observatory.example/app/explore/?organization=" + url.QueryEscape(bootstrap.Organization.ID)
page := perform(handler, http.MethodGet, target, nil, []*http.Cookie{cookie})
pageBody := page.Body.String()
currentLink := `<a aria-current="page" href="/app/explore/?organization=` + bootstrap.Organization.ID + `">Explore</a>`
if page.Code != http.StatusOK || !strings.Contains(pageBody, "Follow the evidence.") || !strings.Contains(pageBody, currentLink) || !strings.Contains(pageBody, defaultExploreQuery) || strings.Contains(pageBody, "Authorized query results") {
t.Fatalf("explore status=%d body=%s", page.Code, pageBody)
}
if !strings.Contains(pageBody, `method="post" action="/app/explore/?organization=`+bootstrap.Organization.ID+`"`) || strings.Contains(pageBody, "?query=") {
t.Fatalf("explore form did not keep query in POST body: %s", pageBody)
}
head := perform(handler, http.MethodHead, target, nil, []*http.Cookie{cookie})
if head.Code != http.StatusOK || head.Body.Len() != 0 || head.Header().Get("Content-Length") != page.Header().Get("Content-Length") {
t.Fatalf("explore HEAD status=%d length=%q body=%d", head.Code, head.Header().Get("Content-Length"), head.Body.Len())
}
now := server.now()
sourceToken, err := store.CreateSource(context.Background(), "explore-source", model.Scope{OrganizationID: bootstrap.Organization.ID, ProjectID: "project-a", EnvironmentID: "production", ServiceID: "service-a"})
if err != nil {
t.Fatal(err)
}
batch := model.Batch{Version: model.BatchVersion, SourceID: "explore-source", StreamID: "requests", Sequence: 1, ObservedAt: now, Signal: model.SignalLogs, Records: []model.Observation{{Timestamp: now, Name: "application.http.request", Attributes: map[string]string{"http.status_code": "200", "http.route": "/explore-proof"}}}}
if _, err = store.Ingest(context.Background(), sourceToken, batch, now); err != nil {
t.Fatal(err)
}
if err = store.Recover(context.Background()); err != nil {
t.Fatal(err)
}
csrf, err := authhttp.CSRFToken(cookie.Value, "query:execute")
if err != nil {
t.Fatal(err)
}
queryText := `logs | where route == "/explore-proof" | window 1h | limit 10`
form := url.Values{"csrf_token": []string{csrf}, "query": []string{queryText}}.Encode()
headers := http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}}
result := perform(handler, http.MethodPost, target, strings.NewReader(form), []*http.Cookie{cookie}, headers)
resultBody := result.Body.String()
for _, required := range []string{"Authorized query results", "/explore-proof", "Scanned rows", "Matched rows", "Scanned bytes", "Execution", html.EscapeString(queryText)} {
if !strings.Contains(resultBody, required) {
t.Fatalf("result omitted %q: status=%d body=%s", required, result.Code, resultBody)
}
}
if result.Code != http.StatusOK || strings.Contains(result.Header().Get("Content-Type"), "application/json") {
t.Fatalf("query status=%d type=%q body=%s", result.Code, result.Header().Get("Content-Type"), resultBody)
}
invalidForm := url.Values{"csrf_token": []string{"invalid"}, "query": []string{"logs | limit 10"}}.Encode()
invalid := perform(handler, http.MethodPost, target, strings.NewReader(invalidForm), []*http.Cookie{cookie}, headers)
if invalid.Code != http.StatusForbidden || !strings.Contains(invalid.Body.String(), "query form expired") || strings.Contains(invalid.Header().Get("Content-Type"), "application/json") {
t.Fatalf("invalid CSRF status=%d body=%s", invalid.Code, invalid.Body.String())
}
badQuery := url.Values{"csrf_token": []string{csrf}, "query": []string{"logs | become unbounded"}}.Encode()
rejected := perform(handler, http.MethodPost, target, strings.NewReader(badQuery), []*http.Cookie{cookie}, headers)
if rejected.Code != http.StatusUnprocessableEntity || !strings.Contains(rejected.Body.String(), "could not be parsed") || !strings.Contains(rejected.Body.String(), "logs | become unbounded") {
t.Fatalf("rejected query status=%d body=%s", rejected.Code, rejected.Body.String())
}
}
func TestHTMLLoginFailsClosed(t *testing.T) {
server, store, identities, _ := newUITestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
wrongOrigin := http.Header{"Origin": []string{"https://attacker.example"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
result := perform(handler, http.MethodPost, "https://observatory.example/login/", strings.NewReader("identifier=operator&password=do-not-echo"), nil, wrongOrigin)
if result.Code != http.StatusForbidden || !strings.Contains(result.Body.String(), loginFormFailure) || strings.Contains(result.Body.String(), "do-not-echo") || strings.Contains(result.Header().Get("Content-Type"), "application/json") {
t.Fatalf("wrong origin status=%d body=%s", result.Code, result.Body.String())
}
csrfCookie, csrfToken := loginFormCSRF(t, handler)
extraField := http.Header{"Origin": []string{"https://observatory.example"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
extraForm := url.Values{"csrf_token": []string{csrfToken}, "identifier": []string{"operator"}, "password": []string{"wrong"}, "next": []string{"https://attacker.example"}}.Encode()
result = perform(handler, http.MethodPost, "https://observatory.example/login/", strings.NewReader(extraForm), []*http.Cookie{csrfCookie}, extraField)
if result.Code != http.StatusForbidden || !strings.Contains(result.Body.String(), loginFormFailure) || strings.Contains(result.Body.String(), "attacker.example") || result.Header().Get("Location") != "" {
t.Fatalf("extra field status=%d location=%q body=%s", result.Code, result.Header().Get("Location"), result.Body.String())
}
}
func TestHTMLLoginUsesTokenWhenBrowserOmitsOrObscuresOriginMetadata(t *testing.T) {
server, store, identities, _ := newUITestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
page := perform(handler, http.MethodGet, "https://observatory.example/login/", nil, nil)
if page.Code != http.StatusOK {
t.Fatalf("login page status=%d body=%s", page.Code, page.Body.String())
}
var csrfCookie *http.Cookie
for _, cookie := range page.Result().Cookies() {
if cookie.Name == loginCSRFCookieName {
csrfCookie = cookie
}
}
if csrfCookie == nil || !csrfCookie.Secure || !csrfCookie.HttpOnly || csrfCookie.SameSite != http.SameSiteStrictMode || csrfCookie.MaxAge != 600 {
t.Fatalf("login CSRF cookie=%+v", csrfCookie)
}
const marker = `name="csrf_token" value="`
start := strings.Index(page.Body.String(), marker)
if start < 0 {
t.Fatalf("login page omitted CSRF token: %s", page.Body.String())
}
start += len(marker)
end := strings.IndexByte(page.Body.String()[start:], '"')
if end < 0 {
t.Fatal("login page CSRF token is unterminated")
}
token := page.Body.String()[start : start+end]
if token == "" || token != csrfCookie.Value {
t.Fatal("login form and cookie CSRF tokens differ")
}
form := url.Values{"csrf_token": []string{token}, "identifier": []string{"not-a-user"}, "password": []string{"not-a-password"}}.Encode()
contentType := http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}}
omittedMetadata := perform(handler, http.MethodPost, "https://observatory.example/login/", strings.NewReader(form), []*http.Cookie{csrfCookie}, contentType)
if omittedMetadata.Code != http.StatusUnauthorized || strings.Contains(omittedMetadata.Body.String(), "not-a-password") {
t.Fatalf("origin-metadata fallback status=%d body=%s", omittedMetadata.Code, omittedMetadata.Body.String())
}
opaqueSameOriginHeaders := contentType.Clone()
opaqueSameOriginHeaders.Set("Origin", "null")
opaqueSameOriginHeaders.Set("Sec-Fetch-Site", "same-origin")
opaqueSameOrigin := perform(handler, http.MethodPost, "https://observatory.example/login/", strings.NewReader(form), []*http.Cookie{csrfCookie}, opaqueSameOriginHeaders)
if opaqueSameOrigin.Code != http.StatusUnauthorized || strings.Contains(opaqueSameOrigin.Body.String(), "not-a-password") {
t.Fatalf("opaque same-origin fallback status=%d body=%s", opaqueSameOrigin.Code, opaqueSameOrigin.Body.String())
}
withoutToken := perform(handler, http.MethodPost, "https://observatory.example/login/", strings.NewReader("identifier=not-a-user&password=not-a-password"), nil, contentType)
if withoutToken.Code != http.StatusForbidden {
t.Fatalf("originless tokenless status=%d body=%s", withoutToken.Code, withoutToken.Body.String())
}
opaqueWithoutToken := perform(handler, http.MethodPost, "https://observatory.example/login/", strings.NewReader("identifier=not-a-user&password=not-a-password"), nil, opaqueSameOriginHeaders)
if opaqueWithoutToken.Code != http.StatusForbidden {
t.Fatalf("opaque tokenless status=%d body=%s", opaqueWithoutToken.Code, opaqueWithoutToken.Body.String())
}
opaqueWithoutFetchMetadata := contentType.Clone()
opaqueWithoutFetchMetadata.Set("Origin", "null")
opaqueTokenOnly := perform(handler, http.MethodPost, "https://observatory.example/login/", strings.NewReader(form), []*http.Cookie{csrfCookie}, opaqueWithoutFetchMetadata)
if opaqueTokenOnly.Code != http.StatusUnauthorized || !strings.Contains(opaqueTokenOnly.Body.String(), loginCredentialFailure) {
t.Fatalf("opaque origin without same-origin fetch metadata status=%d body=%s", opaqueTokenOnly.Code, opaqueTokenOnly.Body.String())
}
wrongOrigin := contentType.Clone()
wrongOrigin.Set("Origin", "https://attacker.example")
crossSite := perform(handler, http.MethodPost, "https://observatory.example/login/", strings.NewReader(form), []*http.Cookie{csrfCookie}, wrongOrigin)
if crossSite.Code != http.StatusForbidden {
t.Fatalf("cross-site token replay status=%d body=%s", crossSite.Code, crossSite.Body.String())
}
}
func TestTemporaryOperatorMustRotatePasswordBeforeUsingApplication(t *testing.T) {
server, store, identities, _ := newRotationTestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
const temporary = "temporary correct horse battery staple"
const replacement = "permanent correct horse battery staple"
headers := http.Header{"Origin": []string{"https://observatory.example"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
login := loginHTMLResponse(t, handler, "operator", temporary)
if login.Code != http.StatusSeeOther || login.Header().Get("Location") != "/account/password/" || strings.Contains(login.Body.String(), temporary) {
t.Fatalf("login status=%d location=%q body=%s", login.Code, login.Header().Get("Location"), login.Body.String())
}
var cookie *http.Cookie
for _, candidate := range login.Result().Cookies() {
if candidate.Name == "__Host-observatory_session" {
cookie = candidate
break
}
}
if cookie == nil {
t.Fatalf("login cookies=%+v", login.Result().Cookies())
}
blocked := perform(handler, http.MethodGet, "https://observatory.example/app/", nil, []*http.Cookie{cookie})
if blocked.Code != http.StatusSeeOther || blocked.Header().Get("Location") != "/account/password/" {
t.Fatalf("blocked app status=%d location=%q", blocked.Code, blocked.Header().Get("Location"))
}
blockedWrite := perform(handler, http.MethodPost, "https://observatory.example/api/v1/query", strings.NewReader(`{}`), []*http.Cookie{cookie}, headers)
if blockedWrite.Code != http.StatusForbidden {
t.Fatalf("blocked write status=%d body=%s", blockedWrite.Code, blockedWrite.Body.String())
}
page := perform(handler, http.MethodGet, "https://observatory.example/account/password/", nil, []*http.Cookie{cookie})
if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), "Choose your password") || strings.Contains(page.Body.String(), temporary) {
t.Fatalf("password page status=%d body=%s", page.Code, page.Body.String())
}
head := perform(handler, http.MethodHead, "https://observatory.example/account/password/", nil, []*http.Cookie{cookie})
if head.Code != http.StatusOK || head.Body.Len() != 0 || head.Header().Get("Content-Length") != page.Header().Get("Content-Length") {
t.Fatalf("password HEAD status=%d length=%q body=%d", head.Code, head.Header().Get("Content-Length"), head.Body.Len())
}
csrf, err := authhttp.CSRFToken(cookie.Value, "account:password:change")
if err != nil {
t.Fatal(err)
}
crossSiteHeaders := http.Header{"Origin": []string{"https://attacker.example"}, "Sec-Fetch-Site": []string{"cross-site"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
crossSiteForm := url.Values{"csrf_token": []string{csrf}, "current_password": []string{temporary}, "new_password": []string{replacement}, "confirm_password": []string{replacement}}.Encode()
crossSite := perform(handler, http.MethodPost, "https://observatory.example/account/password/", strings.NewReader(crossSiteForm), []*http.Cookie{cookie}, crossSiteHeaders)
if crossSite.Code != http.StatusForbidden || !strings.Contains(crossSite.Body.String(), passwordFormFailure) || strings.Contains(crossSite.Body.String(), temporary) || strings.Contains(crossSite.Body.String(), replacement) || strings.Contains(crossSite.Header().Get("Content-Type"), "application/json") {
t.Fatalf("cross-site password status=%d body=%s", crossSite.Code, crossSite.Body.String())
}
invalidTokenForm := url.Values{"csrf_token": []string{"invalid"}, "current_password": []string{temporary}, "new_password": []string{replacement}, "confirm_password": []string{replacement}}.Encode()
invalidToken := perform(handler, http.MethodPost, "https://observatory.example/account/password/", strings.NewReader(invalidTokenForm), []*http.Cookie{cookie}, headers)
if invalidToken.Code != http.StatusForbidden || !strings.Contains(invalidToken.Body.String(), passwordFormFailure) || strings.Contains(invalidToken.Body.String(), temporary) || strings.Contains(invalidToken.Body.String(), replacement) || strings.Contains(invalidToken.Header().Get("Content-Type"), "application/json") {
t.Fatalf("invalid-token password status=%d body=%s", invalidToken.Code, invalidToken.Body.String())
}
mismatch := url.Values{"csrf_token": []string{csrf}, "current_password": []string{temporary}, "new_password": []string{replacement}, "confirm_password": []string{"different password value"}}.Encode()
rejected := perform(handler, http.MethodPost, "https://observatory.example/account/password/", strings.NewReader(mismatch), []*http.Cookie{cookie}, headers)
if rejected.Code != http.StatusUnprocessableEntity || !strings.Contains(rejected.Body.String(), passwordMatchFailure) || strings.Contains(rejected.Body.String(), temporary) || strings.Contains(rejected.Body.String(), replacement) {
t.Fatalf("mismatch status=%d body=%s", rejected.Code, rejected.Body.String())
}
change := url.Values{"csrf_token": []string{csrf}, "current_password": []string{temporary}, "new_password": []string{replacement}, "confirm_password": []string{replacement}}.Encode()
privacyHeaders := http.Header{"Origin": []string{"null"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
changed := perform(handler, http.MethodPost, "https://observatory.example/account/password/", strings.NewReader(change), []*http.Cookie{cookie}, privacyHeaders)
if changed.Code != http.StatusSeeOther || changed.Header().Get("Location") != "/login/?password=changed" {
t.Fatalf("change status=%d location=%q body=%s", changed.Code, changed.Header().Get("Location"), changed.Body.String())
}
oldSession := perform(handler, http.MethodGet, "https://observatory.example/app/", nil, []*http.Cookie{cookie})
if oldSession.Code != http.StatusSeeOther || oldSession.Header().Get("Location") != "/login/" {
t.Fatalf("old session status=%d location=%q", oldSession.Code, oldSession.Header().Get("Location"))
}
if _, _, err = identities.Auth.Authenticate(t.Context(), "operator", temporary, time.Hour); err == nil {
t.Fatal("temporary password remained valid")
}
_, principal, err := identities.Auth.Authenticate(t.Context(), "operator", replacement, time.Hour)
if err != nil || principal.User.PasswordChangeRequired {
t.Fatalf("replacement principal=%+v err=%v", principal, err)
}
newLogin := loginHTMLResponse(t, handler, "operator", replacement)
if newLogin.Code != http.StatusSeeOther || newLogin.Header().Get("Location") != "/app/" {
t.Fatalf("new login status=%d location=%q", newLogin.Code, newLogin.Header().Get("Location"))
}
}
func TestAPITemporaryOperatorReceivesScopedRotationToken(t *testing.T) {
server, store, identities, _ := newRotationTestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
loginHeaders := http.Header{"Origin": []string{"https://observatory.example"}, "Content-Type": []string{"application/json"}}
login := perform(handler, http.MethodPost, "https://observatory.example/api/v1/session", strings.NewReader(`{"identifier":"operator","password":"temporary correct horse battery staple"}`), nil, loginHeaders)
var session struct {
PasswordChangeRequired bool `json:"password_change_required"`
PasswordChangeCSRF string `json:"password_change_csrf"`
}
if login.Code != http.StatusOK || json.Unmarshal(login.Body.Bytes(), &session) != nil || !session.PasswordChangeRequired || session.PasswordChangeCSRF == "" {
t.Fatalf("login status=%d session=%+v body=%s", login.Code, session, login.Body.String())
}
cookies := login.Result().Cookies()
changeHeaders := http.Header{"Origin": []string{"https://observatory.example"}, "Content-Type": []string{"application/json"}, "X-CSRF-Token": []string{session.PasswordChangeCSRF}}
changed := perform(handler, http.MethodPost, "https://observatory.example/api/v1/account/password", strings.NewReader(`{"current_password":"temporary correct horse battery staple","new_password":"API replacement password value"}`), cookies, changeHeaders)
if changed.Code != http.StatusNoContent || changed.Body.Len() != 0 {
t.Fatalf("change status=%d body=%s", changed.Code, changed.Body.String())
}
_, principal, err := identities.Auth.Authenticate(t.Context(), "operator", "API replacement password value", time.Hour)
if err != nil || principal.User.PasswordChangeRequired {
t.Fatalf("principal=%+v err=%v", principal, err)
}
}
func TestLiveRefreshStreamIsAuthorizedAndCarriesNoTelemetry(t *testing.T) {
server, store, identities, bootstrap := newUITestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
unauthorized := perform(handler, http.MethodGet, "https://observatory.example/app/events?organization="+url.QueryEscape(bootstrap.Organization.ID), nil, nil)
if unauthorized.Code != http.StatusUnauthorized {
t.Fatalf("unauthorized stream status=%d", unauthorized.Code)
}
cookie := loginHTML(t, handler)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
request := httptest.NewRequest(http.MethodGet, "https://observatory.example/app/events?organization="+url.QueryEscape(bootstrap.Organization.ID), nil).WithContext(ctx)
request.AddCookie(cookie)
stream := newStreamRecorder()
done := make(chan struct{})
go func() {
handler.ServeHTTP(stream, request)
close(done)
}()
waitFlush := func() {
t.Helper()
select {
case <-stream.flushed:
case <-ctx.Done():
t.Fatal("stream did not flush before timeout")
}
}
waitFlush()
if stream.statusCode() != http.StatusOK || stream.Header().Get("Content-Type") != "text/event-stream; charset=utf-8" || stream.Header().Get("X-Accel-Buffering") != "no" || stream.bodyString() != "event: ready\ndata: {}\n\n" {
t.Fatalf("stream status=%d headers=%v body=%q", stream.statusCode(), stream.Header(), stream.bodyString())
}
server.refresh.publish(bootstrap.Organization.ID)
waitFlush()
streamBody := stream.bodyString()
if streamBody != "event: ready\ndata: {}\n\nevent: refresh\ndata: {}\n\n" || strings.Contains(streamBody, bootstrap.Organization.ID) || strings.Contains(streamBody, "service") {
t.Fatalf("stream body=%q", streamBody)
}
cancel()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("stream did not stop after cancellation")
}
}
func TestDashboardManagementIsScopedCSRFProtectedAndExportable(t *testing.T) {
server, store, identities, bootstrap := newUITestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
cookie := loginHTML(t, handler)
csrf, err := authhttp.CSRFToken(cookie.Value, "dashboards:manage")
if err != nil {
t.Fatal(err)
}
headers := http.Header{"Origin": []string{"https://observatory.example"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
invalid := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{"invalid"},
"name": []string{"Recent errors"}, "description": []string{"A bounded recent error view."},
"query": []string{"logs | where status >= 500 | window 1h | limit 50"},
}.Encode()
denied := perform(handler, http.MethodPost, "https://observatory.example/app/queries/", strings.NewReader(invalid), []*http.Cookie{cookie}, headers)
if denied.Code != http.StatusForbidden {
t.Fatalf("invalid CSRF status=%d", denied.Code)
}
queryForm := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"name": []string{"Recent errors"}, "description": []string{"A bounded recent error view."},
"query": []string{"logs | where status >= 500 | window 1h | limit 50"},
}.Encode()
privacyHeaders := http.Header{"Origin": []string{"null"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
createdQuery := perform(handler, http.MethodPost, "https://observatory.example/app/queries/", strings.NewReader(queryForm), []*http.Cookie{cookie}, privacyHeaders)
if createdQuery.Code != http.StatusSeeOther || !strings.HasPrefix(createdQuery.Header().Get("Location"), "/app/?organization=") {
t.Fatalf("query status=%d location=%q body=%s", createdQuery.Code, createdQuery.Header().Get("Location"), createdQuery.Body.String())
}
queries, err := store.SavedQueries(context.Background(), bootstrap.Organization.ID)
if err != nil || len(queries) != 1 {
t.Fatalf("queries=%+v err=%v", queries, err)
}
mismatchedDashboard := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"slug": []string{"invalid-stat"}, "name": []string{"Invalid stat"},
"description": []string{"A non-summary query cannot become a statistic."}, "panel_title": []string{"Invalid"},
"saved_query_id": []string{queries[0].ID}, "visualization": []string{"stat"},
}.Encode()
mismatched := perform(handler, http.MethodPost, "https://observatory.example/app/dashboards/", strings.NewReader(mismatchedDashboard), []*http.Cookie{cookie}, headers)
if mismatched.Code != http.StatusUnprocessableEntity {
t.Fatalf("mismatched presentation status=%d body=%s", mismatched.Code, mismatched.Body.String())
}
dashboardForm := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"slug": []string{"recent-errors"}, "name": []string{"Recent errors"},
"description": []string{"An accessible bounded error dashboard."}, "panel_title": []string{"Errors"},
"saved_query_id": []string{queries[0].ID}, "visualization": []string{"table"},
}.Encode()
createdDashboard := perform(handler, http.MethodPost, "https://observatory.example/app/dashboards/", strings.NewReader(dashboardForm), []*http.Cookie{cookie}, headers)
if createdDashboard.Code != http.StatusSeeOther || !strings.HasPrefix(createdDashboard.Header().Get("Location"), "/app/dashboards/recent-errors/") {
t.Fatalf("dashboard status=%d location=%q body=%s", createdDashboard.Code, createdDashboard.Header().Get("Location"), createdDashboard.Body.String())
}
target := "https://observatory.example/app/dashboards/recent-errors/?organization=" + url.QueryEscape(bootstrap.Organization.ID)
dashboard := perform(handler, http.MethodGet, target, nil, []*http.Cookie{cookie})
if dashboard.Code != http.StatusOK || !strings.Contains(dashboard.Body.String(), "An accessible bounded error dashboard.") || !strings.Contains(dashboard.Body.String(), "Recent errors") || !strings.Contains(dashboard.Body.String(), ">Errors</h2>") {
t.Fatalf("dashboard status=%d body=%s", dashboard.Code, dashboard.Body.String())
}
dashboardHead := perform(handler, http.MethodHead, target, nil, []*http.Cookie{cookie})
if dashboardHead.Code != http.StatusOK || dashboardHead.Body.Len() != 0 || dashboardHead.Header().Get("Content-Length") != dashboard.Header().Get("Content-Length") {
t.Fatalf("dashboard HEAD status=%d length=%q", dashboardHead.Code, dashboardHead.Header().Get("Content-Length"))
}
storedDashboard, err := store.Dashboard(context.Background(), bootstrap.Organization.ID, "recent-errors")
if err != nil || storedDashboard.Revision != 1 || !strings.Contains(dashboard.Body.String(), "Update dashboard details") || !strings.Contains(dashboard.Body.String(), `name="expected_revision" value="1"`) {
t.Fatalf("stored dashboard=%+v err=%v body=%s", storedDashboard, err, dashboard.Body.String())
}
revisionForm := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"dashboard_id": []string{storedDashboard.ID}, "expected_revision": []string{strconv.Itoa(storedDashboard.Revision)},
"slug": []string{storedDashboard.Slug}, "name": []string{"Current errors"},
"description": []string{"A revision-safe bounded error dashboard."},
}.Encode()
revised := perform(handler, http.MethodPost, target, strings.NewReader(revisionForm), []*http.Cookie{cookie}, headers)
if revised.Code != http.StatusSeeOther {
t.Fatalf("revision status=%d body=%s", revised.Code, revised.Body.String())
}
stale := perform(handler, http.MethodPost, target, strings.NewReader(revisionForm), []*http.Cookie{cookie}, headers)
if stale.Code != http.StatusConflict {
t.Fatalf("stale revision status=%d body=%s", stale.Code, stale.Body.String())
}
storedDashboard, err = store.Dashboard(context.Background(), bootstrap.Organization.ID, "recent-errors")
if err != nil || storedDashboard.Revision != 2 || storedDashboard.Name != "Current errors" {
t.Fatalf("revised dashboard=%+v err=%v", storedDashboard, err)
}
addPanelForm := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"dashboard_id": []string{storedDashboard.ID}, "expected_revision": []string{strconv.Itoa(storedDashboard.Revision)},
"panel_title": []string{"Recent failures"}, "saved_query_id": []string{queries[0].ID}, "visualization": []string{"table"},
}.Encode()
addTarget := "https://observatory.example/app/dashboards/recent-errors/panels/?organization=" + url.QueryEscape(bootstrap.Organization.ID)
added := perform(handler, http.MethodPost, addTarget, strings.NewReader(addPanelForm), []*http.Cookie{cookie}, headers)
if added.Code != http.StatusSeeOther {
t.Fatalf("add panel status=%d body=%s", added.Code, added.Body.String())
}
storedDashboard, err = store.Dashboard(context.Background(), bootstrap.Organization.ID, "recent-errors")
if err != nil || storedDashboard.Revision != 3 || len(storedDashboard.Panels) != 2 {
t.Fatalf("dashboard after add=%+v err=%v", storedDashboard, err)
}
addedPanel := storedDashboard.Panels[1]
updatePanelForm := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"dashboard_id": []string{storedDashboard.ID}, "expected_revision": []string{strconv.Itoa(storedDashboard.Revision)},
"panel_title": []string{"Renamed failures"}, "saved_query_id": []string{queries[0].ID}, "visualization": []string{"table"},
}.Encode()
updatePanelTarget := "https://observatory.example/app/dashboards/recent-errors/panels/" + url.PathEscape(addedPanel.ID) + "/?organization=" + url.QueryEscape(bootstrap.Organization.ID)
updatedPanel := perform(handler, http.MethodPost, updatePanelTarget, strings.NewReader(updatePanelForm), []*http.Cookie{cookie}, headers)
if updatedPanel.Code != http.StatusSeeOther {
t.Fatalf("update panel status=%d body=%s", updatedPanel.Code, updatedPanel.Body.String())
}
storedDashboard, err = store.Dashboard(context.Background(), bootstrap.Organization.ID, "recent-errors")
if err != nil || storedDashboard.Revision != 4 || storedDashboard.Panels[1].Title != "Renamed failures" {
t.Fatalf("dashboard after panel update=%+v err=%v", storedDashboard, err)
}
mismatchedRevision := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"dashboard_id": []string{storedDashboard.ID}, "expected_revision": []string{strconv.Itoa(storedDashboard.Revision)},
"panel_title": []string{"Invalid chart"}, "saved_query_id": []string{queries[0].ID}, "visualization": []string{"timeseries"},
}.Encode()
mismatchedPanel := perform(handler, http.MethodPost, updatePanelTarget, strings.NewReader(mismatchedRevision), []*http.Cookie{cookie}, headers)
if mismatchedPanel.Code != http.StatusUnprocessableEntity {
t.Fatalf("mismatched panel status=%d body=%s", mismatchedPanel.Code, mismatchedPanel.Body.String())
}
removePanelForm := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"dashboard_id": []string{storedDashboard.ID}, "expected_revision": []string{strconv.Itoa(storedDashboard.Revision)},
}.Encode()
removeTarget := "https://observatory.example/app/dashboards/recent-errors/panels/" + url.PathEscape(addedPanel.ID) + "/remove/?organization=" + url.QueryEscape(bootstrap.Organization.ID)
removed := perform(handler, http.MethodPost, removeTarget, strings.NewReader(removePanelForm), []*http.Cookie{cookie}, headers)
if removed.Code != http.StatusSeeOther {
t.Fatalf("remove panel status=%d body=%s", removed.Code, removed.Body.String())
}
storedDashboard, err = store.Dashboard(context.Background(), bootstrap.Organization.ID, "recent-errors")
if err != nil || storedDashboard.Revision != 5 || len(storedDashboard.Panels) != 1 {
t.Fatalf("dashboard after remove=%+v err=%v", storedDashboard, err)
}
exportTarget := "https://observatory.example/app/dashboards/recent-errors/export.json?organization=" + url.QueryEscape(bootstrap.Organization.ID)
exported := perform(handler, http.MethodGet, exportTarget, nil, []*http.Cookie{cookie})
if exported.Code != http.StatusOK || exported.Header().Get("Content-Type") != "application/json" || !strings.Contains(exported.Body.String(), `"version": 1`) || strings.Contains(exported.Body.String(), bootstrap.Organization.ID) || strings.Contains(exported.Body.String(), bootstrap.User.ID) {
t.Fatalf("export status=%d headers=%v body=%s", exported.Code, exported.Header(), exported.Body.String())
}
exportedHead := perform(handler, http.MethodHead, exportTarget, nil, []*http.Cookie{cookie})
if exportedHead.Code != http.StatusOK || exportedHead.Body.Len() != 0 || exportedHead.Header().Get("Content-Length") != exported.Header().Get("Content-Length") {
t.Fatalf("export HEAD status=%d length=%q", exportedHead.Code, exportedHead.Header().Get("Content-Length"))
}
unauthorized := perform(handler, http.MethodGet, exportTarget, nil, nil)
if unauthorized.Code != http.StatusUnauthorized || unauthorized.Body.String() == exported.Body.String() {
t.Fatalf("unauthorized export status=%d", unauthorized.Code)
}
}
func TestIncidentRulesEvaluationInboxAndResponseAreScoped(t *testing.T) {
server, store, identities, bootstrap := newUITestServer(t)
defer store.Close()
defer identities.Close()
pushes := &recordingPushDispatcher{}
server.options.PushDispatcher = pushes
server.options.PushPublicKey = base64.RawURLEncoding.EncodeToString(append([]byte{4}, make([]byte, 64)...))
handler := server.Handler()
cookie := loginHTML(t, handler)
now := server.now()
saved, err := store.SaveQuery(context.Background(), storage.SavedQueryInput{
OrganizationID: bootstrap.Organization.ID, ActorUserID: bootstrap.User.ID, MaxRows: 100,
Name: "Recent failures", Description: "Recent HTTP failures.", Query: "logs | where status >= 500 | window 1h | limit 50",
}, now)
if err != nil {
t.Fatal(err)
}
token, err := store.CreateSource(context.Background(), "incident-source", model.Scope{OrganizationID: bootstrap.Organization.ID, ProjectID: "project-a", EnvironmentID: "production", ServiceID: "service-a"})
if err != nil {
t.Fatal(err)
}
batch := model.Batch{Version: model.BatchVersion, SourceID: "incident-source", StreamID: "requests", Sequence: 1, ObservedAt: now, Signal: model.SignalLogs, Records: []model.Observation{{Timestamp: now, Name: "http.request", Attributes: map[string]string{"http.status_code": "503", "http.route": "/failed"}}}}
if _, err = store.Ingest(context.Background(), token, batch, now); err != nil {
t.Fatal(err)
}
if err = store.Recover(context.Background()); err != nil {
t.Fatal(err)
}
csrf, err := authhttp.CSRFToken(cookie.Value, "incidents:manage")
if err != nil {
t.Fatal(err)
}
headers := http.Header{"Origin": []string{"https://observatory.example"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
form := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"name": []string{"HTTP failures"}, "description": []string{"Open when a bounded saved query finds a failure."},
"saved_query_id": []string{saved.ID}, "severity": []string{"critical"}, "minimum_matches": []string{"1"},
"required_consecutive": []string{"1"}, "evaluation_interval": []string{"15s"},
}
invalid := cloneValues(form)
invalid.Set("csrf_token", "invalid")
denied := perform(handler, http.MethodPost, "https://observatory.example/app/alert-rules/", strings.NewReader(invalid.Encode()), []*http.Cookie{cookie}, headers)
if denied.Code != http.StatusForbidden {
t.Fatalf("invalid CSRF status=%d body=%s", denied.Code, denied.Body.String())
}
privacyHeaders := http.Header{"Origin": []string{"null"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
created := perform(handler, http.MethodPost, "https://observatory.example/app/alert-rules/", strings.NewReader(form.Encode()), []*http.Cookie{cookie}, privacyHeaders)
if created.Code != http.StatusSeeOther || !strings.HasPrefix(created.Header().Get("Location"), "/app/incidents/?organization=") {
t.Fatalf("create status=%d location=%q body=%s", created.Code, created.Header().Get("Location"), created.Body.String())
}
updates, remove, err := server.refresh.subscribe(bootstrap.Organization.ID)
if err != nil {
t.Fatal(err)
}
defer remove()
if evaluated, evaluationErr := server.EvaluateAlerts(context.Background()); evaluationErr != nil || evaluated != 1 {
t.Fatalf("evaluated=%d err=%v", evaluated, evaluationErr)
}
if len(pushes.organizations) != 1 || pushes.organizations[0] != bootstrap.Organization.ID {
t.Fatalf("push organizations=%v", pushes.organizations)
}
select {
case <-updates:
default:
t.Fatal("incident change did not publish a generic refresh")
}
incidents, err := store.Incidents(context.Background(), bootstrap.Organization.ID, false, 10)
if err != nil || len(incidents) != 1 || incidents[0].State != "firing" {
t.Fatalf("incidents=%+v err=%v", incidents, err)
}
path := "https://observatory.example/app/incidents/?organization=" + url.QueryEscape(bootstrap.Organization.ID)
inbox := perform(handler, http.MethodGet, path, nil, []*http.Cookie{cookie})
if inbox.Code != http.StatusOK || !strings.Contains(inbox.Body.String(), "What needs attention?") || !strings.Contains(inbox.Body.String(), "HTTP failures") || !strings.Contains(inbox.Body.String(), "critical · firing") || !strings.Contains(inbox.Body.String(), "data-cache-inbox") || !strings.Contains(inbox.Body.String(), "data-push-toggle") || !strings.Contains(inbox.Body.String(), `data-open-incident-count="1"`) || strings.Contains(inbox.Body.String(), "/failed") {
t.Fatalf("inbox status=%d body=%s", inbox.Code, inbox.Body.String())
}
pushCSRF, err := authhttp.CSRFToken(cookie.Value, "push:manage")
if err != nil {
t.Fatal(err)
}
clientKey, err := ecdh.P256().GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
authSecret := make([]byte, 16)
if _, err = rand.Read(authSecret); err != nil {
t.Fatal(err)
}
pushBody, _ := json.Marshal(map[string]any{"organization_id": bootstrap.Organization.ID, "endpoint": "https://push.example.test/send/browser", "keys": map[string]string{"p256dh": base64.RawURLEncoding.EncodeToString(clientKey.PublicKey().Bytes()), "auth": base64.RawURLEncoding.EncodeToString(authSecret)}})
pushHeaders := make(http.Header)
pushHeaders.Set("Origin", "https://observatory.example")
pushHeaders.Set("Content-Type", "application/json")
pushHeaders.Set("X-CSRF-Token", pushCSRF)
invalidPushHeaders := pushHeaders.Clone()
invalidPushHeaders.Set("X-CSRF-Token", "invalid")
invalidPush := perform(handler, http.MethodPost, "https://observatory.example/api/v1/push/subscription", bytes.NewReader(pushBody), []*http.Cookie{cookie}, invalidPushHeaders)
if invalidPush.Code != http.StatusForbidden {
t.Fatalf("invalid push CSRF status=%d body=%s", invalidPush.Code, invalidPush.Body.String())
}
crossOriginPushHeaders := pushHeaders.Clone()
crossOriginPushHeaders.Set("Origin", "https://attacker.example")
crossOriginPush := perform(handler, http.MethodPost, "https://observatory.example/api/v1/push/subscription", bytes.NewReader(pushBody), []*http.Cookie{cookie}, crossOriginPushHeaders)
if crossOriginPush.Code != http.StatusForbidden {
t.Fatalf("cross-origin push status=%d body=%s", crossOriginPush.Code, crossOriginPush.Body.String())
}
registered := perform(handler, http.MethodPost, "https://observatory.example/api/v1/push/subscription", bytes.NewReader(pushBody), []*http.Cookie{cookie}, pushHeaders)
if registered.Code != http.StatusCreated || !strings.Contains(registered.Body.String(), `"id":"push_`) {
t.Fatalf("push registration status=%d body=%s", registered.Code, registered.Body.String())
}
if subscriptions, listErr := store.PushSubscriptions(context.Background(), bootstrap.Organization.ID); listErr != nil || len(subscriptions) != 1 || subscriptions[0].UserID != bootstrap.User.ID {
t.Fatalf("push subscriptions=%+v err=%v", subscriptions, listErr)
}
statusBody, _ := json.Marshal(map[string]any{"organization_id": bootstrap.Organization.ID, "endpoint": "https://push.example.test/send/browser", "keys": map[string]string{"p256dh": "", "auth": ""}})
pushStatus := perform(handler, http.MethodPost, "https://observatory.example/api/v1/push/subscription/status", bytes.NewReader(statusBody), []*http.Cookie{cookie}, pushHeaders)
if pushStatus.Code != http.StatusOK || !strings.Contains(pushStatus.Body.String(), `"subscribed":true`) {
t.Fatalf("push status=%d body=%s", pushStatus.Code, pushStatus.Body.String())
}
privateEndpointBody := bytes.Replace(pushBody, []byte("https://push.example.test/send/browser"), []byte("https://127.0.0.1/send/browser"), 1)
rejected := perform(handler, http.MethodPost, "https://observatory.example/api/v1/push/subscription", bytes.NewReader(privateEndpointBody), []*http.Cookie{cookie}, pushHeaders)
if rejected.Code != http.StatusUnprocessableEntity || strings.Contains(rejected.Body.String(), "127.0.0.1") {
t.Fatalf("private endpoint status=%d body=%s", rejected.Code, rejected.Body.String())
}
invalidCurveBody, _ := json.Marshal(map[string]any{"organization_id": bootstrap.Organization.ID, "endpoint": "https://push.example.test/send/invalid-curve", "keys": map[string]string{"p256dh": base64.RawURLEncoding.EncodeToString(append([]byte{4}, make([]byte, 64)...)), "auth": base64.RawURLEncoding.EncodeToString(authSecret)}})
invalidCurve := perform(handler, http.MethodPost, "https://observatory.example/api/v1/push/subscription", bytes.NewReader(invalidCurveBody), []*http.Cookie{cookie}, pushHeaders)
if invalidCurve.Code != http.StatusUnprocessableEntity {
t.Fatalf("invalid curve status=%d body=%s", invalidCurve.Code, invalidCurve.Body.String())
}
deleteBody, _ := json.Marshal(map[string]any{"organization_id": bootstrap.Organization.ID, "endpoint": "https://push.example.test/send/browser", "keys": map[string]string{"p256dh": "", "auth": ""}})
deleted := perform(handler, http.MethodDelete, "https://observatory.example/api/v1/push/subscription", bytes.NewReader(deleteBody), []*http.Cookie{cookie}, pushHeaders)
if deleted.Code != http.StatusOK || !strings.Contains(deleted.Body.String(), `"remaining":false`) {
t.Fatalf("push deletion status=%d body=%s", deleted.Code, deleted.Body.String())
}
offlineInbox := perform(handler, http.MethodGet, "https://observatory.example/app/incidents/offline/?organization="+url.QueryEscape(bootstrap.Organization.ID), nil, []*http.Cookie{cookie})
if offlineInbox.Code != http.StatusOK || !strings.Contains(offlineInbox.Body.String(), "Saved incident inbox") || !strings.Contains(offlineInbox.Body.String(), "HTTP failures") {
t.Fatalf("offline inbox status=%d body=%s", offlineInbox.Code, offlineInbox.Body.String())
}
for _, forbidden := range []string{incidents[0].ID, saved.Query, bootstrap.User.ID, "csrf_token", "/failed", "Acknowledge", "Resolve"} {
if strings.Contains(offlineInbox.Body.String(), forbidden) {
t.Fatalf("offline inbox exposed %q: %s", forbidden, offlineInbox.Body.String())
}
}
unauthenticatedOffline := perform(handler, http.MethodGet, "https://observatory.example/app/incidents/offline/?organization="+url.QueryEscape(bootstrap.Organization.ID), nil, nil)
if unauthenticatedOffline.Code != http.StatusSeeOther || unauthenticatedOffline.Header().Get("Location") != "/login/" {
t.Fatalf("unauthenticated offline status=%d", unauthenticatedOffline.Code)
}
inboxHead := perform(handler, http.MethodHead, path, nil, []*http.Cookie{cookie})
if inboxHead.Code != http.StatusOK || inboxHead.Body.Len() != 0 || inboxHead.Header().Get("Content-Length") != inbox.Header().Get("Content-Length") {
t.Fatalf("inbox HEAD status=%d length=%q body=%d", inboxHead.Code, inboxHead.Header().Get("Content-Length"), inboxHead.Body.Len())
}
missingOrganization := perform(handler, http.MethodGet, "https://observatory.example/app/incidents/", nil, []*http.Cookie{cookie})
if missingOrganization.Code != http.StatusBadRequest {
t.Fatalf("missing organization status=%d", missingOrganization.Code)
}
action := url.Values{"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf}, "action": []string{"acknowledge"}, "silence_duration": []string{""}}.Encode()
acknowledged := perform(handler, http.MethodPost, "https://observatory.example/app/incidents/"+incidents[0].ID+"/", strings.NewReader(action), []*http.Cookie{cookie}, headers)
if acknowledged.Code != http.StatusSeeOther {
t.Fatalf("acknowledge status=%d body=%s", acknowledged.Code, acknowledged.Body.String())
}
current, err := store.Incidents(context.Background(), bootstrap.Organization.ID, false, 10)
if err != nil || len(current) != 1 || current[0].State != "acknowledged" || current[0].AcknowledgedBy != bootstrap.User.ID {
t.Fatalf("current=%+v err=%v", current, err)
}
}
type recordingPushDispatcher struct{ organizations []string }
func (dispatcher *recordingPushDispatcher) Enqueue(organizationID string) bool {
dispatcher.organizations = append(dispatcher.organizations, organizationID)
return true
}
func cloneValues(input url.Values) url.Values {
result := make(url.Values, len(input))
for key, values := range input {
result[key] = append([]string(nil), values...)
}
return result
}
func TestAssistedQueryBuilderCreatesTypedTimeSeriesWithTableAlternative(t *testing.T) {
server, store, identities, bootstrap := newUITestServer(t)
defer store.Close()
defer identities.Close()
handler := server.Handler()
cookie := loginHTML(t, handler)
csrf, err := authhttp.CSRFToken(cookie.Value, "dashboards:manage")
if err != nil {
t.Fatal(err)
}
now := server.now()
token, err := store.CreateSource(context.Background(), "builder-source", model.Scope{OrganizationID: bootstrap.Organization.ID, ProjectID: "project-a", EnvironmentID: "production", ServiceID: "service-a"})
if err != nil {
t.Fatal(err)
}
batch := model.Batch{Version: model.BatchVersion, SourceID: "builder-source", StreamID: "requests", Sequence: 1, ObservedAt: now, Signal: model.SignalLogs, Records: []model.Observation{
{Timestamp: now.Add(-6 * time.Minute), Name: "application.http.request", Attributes: map[string]string{"http.status_code": "503", "http.route": "/failed"}},
{Timestamp: now.Add(-1 * time.Minute), Name: "application.http.request", Attributes: map[string]string{"http.status_code": "500", "http.route": "/failed"}},
}}
if _, err = store.Ingest(context.Background(), token, batch, now); err != nil {
t.Fatal(err)
}
if err = store.Recover(context.Background()); err != nil {
t.Fatal(err)
}
headers := http.Header{"Origin": []string{"https://observatory.example"}, "Content-Type": []string{"application/x-www-form-urlencoded"}}
builderForm := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"name": []string{"Errors over time"}, "description": []string{"Five-minute error counts."},
"signal": []string{"logs"}, "filter_field": []string{"status"}, "filter_operator": []string{">="}, "filter_value": []string{"500"},
"window": []string{"1h"}, "aggregate": []string{"count"}, "aggregate_field": []string{""}, "group_by": []string{"route"}, "bucket": []string{"5m"}, "limit": []string{"50"},
}.Encode()
createdQuery := perform(handler, http.MethodPost, "https://observatory.example/app/queries/builder/", strings.NewReader(builderForm), []*http.Cookie{cookie}, headers)
if createdQuery.Code != http.StatusSeeOther {
t.Fatalf("builder status=%d body=%s", createdQuery.Code, createdQuery.Body.String())
}
queries, err := store.SavedQueries(context.Background(), bootstrap.Organization.ID)
if err != nil || len(queries) != 1 {
t.Fatalf("queries=%+v err=%v", queries, err)
}
expectedText := `logs | where status >= "500" | window 1h | summarize count() by route, window(5m) | limit 50`
if queries[0].Query != expectedText || queries[0].AST.Signal != model.SignalLogs || len(queries[0].AST.Filters) != 1 || queries[0].AST.Filters[0].Value != "500" || queries[0].AST.Summary == nil || queries[0].AST.Bucket != 5*time.Minute {
t.Fatalf("saved query=%+v", queries[0])
}
dashboardForm := url.Values{
"organization_id": []string{bootstrap.Organization.ID}, "csrf_token": []string{csrf},
"slug": []string{"error-rate"}, "name": []string{"Error rate"}, "description": []string{"A bounded error trend."},
"panel_title": []string{"Errors by route"}, "saved_query_id": []string{queries[0].ID}, "visualization": []string{"timeseries"},
}.Encode()
createdDashboard := perform(handler, http.MethodPost, "https://observatory.example/app/dashboards/", strings.NewReader(dashboardForm), []*http.Cookie{cookie}, headers)
if createdDashboard.Code != http.StatusSeeOther {
t.Fatalf("dashboard status=%d body=%s", createdDashboard.Code, createdDashboard.Body.String())
}
target := "https://observatory.example/app/dashboards/error-rate/?organization=" + url.QueryEscape(bootstrap.Organization.ID)
dashboard := perform(handler, http.MethodGet, target, nil, []*http.Cookie{cookie})
body := dashboard.Body.String()
if dashboard.Code != http.StatusOK || !strings.Contains(body, "Errors by route visual summary") || strings.Count(body, "<meter ") != 2 || !strings.Contains(body, "<table>") || !strings.Contains(body, "<caption>Errors by route</caption>") {
t.Fatalf("dashboard status=%d body=%s", dashboard.Code, body)
}
}
func TestAssistedQueryBuilderEscapesStageSeparatorsAndRejectsInvalidCombinations(t *testing.T) {
values := url.Values{
"signal": []string{"logs"}, "filter_field": []string{"name"}, "filter_operator": []string{"=="}, "filter_value": []string{"worker | limit 250"},
"window": []string{"1h"}, "aggregate": []string{"none"}, "aggregate_field": []string{""}, "group_by": []string{""}, "bucket": []string{""}, "limit": []string{"50"},
}
text, err := buildAssistedQuery(values, 1000)
if err != nil || strings.Contains(text, `"worker | limit 250"`) || !strings.Contains(text, `\u007c`) {
t.Fatalf("text=%q err=%v", text, err)
}
ast, err := query.Parse(text, 1000)
if err != nil || len(ast.Filters) != 1 || ast.Filters[0].Value != "worker | limit 250" || ast.Limit != 50 {
t.Fatalf("AST=%+v err=%v", ast, err)
}
values.Set("bucket", "5m")
if _, err = buildAssistedQuery(values, 1000); err == nil {
t.Fatal("time bucket without an aggregate was accepted")
}
}
func TestResultChartIsBoundedAndFailsClosedForNegativeValues(t *testing.T) {
result := query.Result{Columns: []query.Column{{Field: "window_start", Type: schema.TypeTime}, {Field: "count", Type: schema.TypeInteger}}}
for index := range 60 {
label := fmt.Sprintf("2026-08-17T07:%02d:00Z", index)
value := strconv.Itoa(index)
result.Rows = append(result.Rows, query.Row{Values: []*string{&label, &value}})
}
chart := resultChart("Requests", result)
if len(chart.Points) != 48 || chart.Points[47].Maximum != "47" || chart.Points[47].Value != "47" {
t.Fatalf("chart=%+v", chart)
}
negative := "-1"
result.Rows[0].Values[1] = &negative
if chart = resultChart("Requests", result); len(chart.Points) != 0 {
t.Fatalf("negative chart=%+v", chart)
}
}
type streamRecorder struct {
mu sync.Mutex
header http.Header
status int
body bytes.Buffer
flushed chan struct{}
}
func newStreamRecorder() *streamRecorder {
return &streamRecorder{header: make(http.Header), flushed: make(chan struct{}, 4)}
}
func (recorder *streamRecorder) Header() http.Header { return recorder.header }
func (recorder *streamRecorder) WriteHeader(status int) {
recorder.mu.Lock()
defer recorder.mu.Unlock()
if recorder.status == 0 {
recorder.status = status
}
}
func (recorder *streamRecorder) Write(body []byte) (int, error) {
recorder.mu.Lock()
defer recorder.mu.Unlock()
if recorder.status == 0 {
recorder.status = http.StatusOK
}
return recorder.body.Write(body)
}
func (recorder *streamRecorder) Flush() {
select {
case recorder.flushed <- struct{}{}:
default:
}
}
func (recorder *streamRecorder) statusCode() int {
recorder.mu.Lock()
defer recorder.mu.Unlock()
return recorder.status
}
func (recorder *streamRecorder) bodyString() string {
recorder.mu.Lock()
defer recorder.mu.Unlock()
return recorder.body.String()
}
func newUITestServer(t *testing.T) (*Server, *storage.Store, *identity.Services, identity.BootstrapResult) {
t.Helper()
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
identities, err := identity.Open(root)
if err != nil {
store.Close()
t.Fatal(err)
}
bootstrap, err := identities.Bootstrap(context.Background(), identity.BootstrapInput{Username: "operator", Email: "operator@example.test", DisplayName: "Operator", Password: "correct horse battery staple"})
if err != nil {
identities.Close()
store.Close()
t.Fatal(err)
}
server, err := New(store, identities, testOptions())
if err != nil {
identities.Close()
store.Close()
t.Fatal(err)
}
server.now = func() time.Time { return time.Date(2026, 8, 17, 7, 30, 0, 0, time.UTC) }
return server, store, identities, bootstrap
}
func newRotationTestServer(t *testing.T) (*Server, *storage.Store, *identity.Services, identity.BootstrapResult) {
t.Helper()
root := filepath.Join(t.TempDir(), "data")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
store, err := storage.Open(root)
if err != nil {
t.Fatal(err)
}
identities, err := identity.Open(root)
if err != nil {
store.Close()
t.Fatal(err)
}
bootstrap, err := identities.Bootstrap(t.Context(), identity.BootstrapInput{Username: "operator", Email: "operator@example.test", DisplayName: "Operator", Password: "temporary correct horse battery staple", RequirePasswordChange: true})
if err != nil {
identities.Close()
store.Close()
t.Fatal(err)
}
server, err := New(store, identities, testOptions())
if err != nil {
identities.Close()
store.Close()
t.Fatal(err)
}
server.now = func() time.Time { return time.Date(2026, 8, 18, 5, 30, 0, 0, time.UTC) }
return server, store, identities, bootstrap
}
func loginHTML(t *testing.T, handler http.Handler) *http.Cookie {
t.Helper()
result := loginHTMLResponse(t, handler, "operator", "correct horse battery staple")
if result.Code != http.StatusSeeOther || result.Header().Get("Location") != "/app/" {
t.Fatalf("login status=%d location=%q body=%s", result.Code, result.Header().Get("Location"), result.Body.String())
}
for _, cookie := range result.Result().Cookies() {
if cookie.Name == "__Host-observatory_session" && cookie.Secure && cookie.HttpOnly && cookie.SameSite == http.SameSiteStrictMode {
return cookie
}
}
t.Fatalf("login cookies=%+v", result.Result().Cookies())
return nil
}
func loginHTMLResponse(t *testing.T, handler http.Handler, identifier, password string) *httptest.ResponseRecorder {
t.Helper()
csrfCookie, csrfToken := loginFormCSRF(t, handler)
form := url.Values{"csrf_token": []string{csrfToken}, "identifier": []string{identifier}, "password": []string{password}}.Encode()
headers := http.Header{"Origin": []string{"https://observatory.example"}, "Content-Type": []string{"application/x-www-form-urlencoded; charset=utf-8"}}
return perform(handler, http.MethodPost, "https://observatory.example/login/", strings.NewReader(form), []*http.Cookie{csrfCookie}, headers)
}
func loginFormCSRF(t *testing.T, handler http.Handler) (*http.Cookie, string) {
t.Helper()
page := perform(handler, http.MethodGet, "https://observatory.example/login/", nil, nil)
if page.Code != http.StatusOK {
t.Fatalf("login page status=%d body=%s", page.Code, page.Body.String())
}
var csrfCookie *http.Cookie
for _, cookie := range page.Result().Cookies() {
if cookie.Name == loginCSRFCookieName {
csrfCookie = cookie
break
}
}
if csrfCookie == nil || !csrfCookie.Secure || !csrfCookie.HttpOnly || csrfCookie.SameSite != http.SameSiteStrictMode {
t.Fatalf("login CSRF cookie=%+v", csrfCookie)
}
const marker = `name="csrf_token" value="`
start := strings.Index(page.Body.String(), marker)
if start < 0 {
t.Fatalf("login page omitted CSRF token: %s", page.Body.String())
}
start += len(marker)
end := strings.IndexByte(page.Body.String()[start:], '"')
if end < 0 {
t.Fatal("login page CSRF token is unterminated")
}
token := page.Body.String()[start : start+end]
if token == "" || token != csrfCookie.Value {
t.Fatal("login form and cookie CSRF tokens differ")
}
return csrfCookie, token
}
func perform(handler http.Handler, method, target string, body io.Reader, cookies []*http.Cookie, headerSets ...http.Header) *httptest.ResponseRecorder {
request := httptest.NewRequest(method, target, body)
for _, cookie := range cookies {
request.AddCookie(cookie)
}
for _, headers := range headerSets {
for name, values := range headers {
for _, value := range values {
request.Header.Add(name, value)
}
}
}
result := httptest.NewRecorder()
handler.ServeHTTP(result, request)
return result
}