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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user