mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
o11y carried a second identity system: email/password sign-in, Google,
OIDC and SAML callbacks, self-registration, invites, reset-password
tokens, roles-on-users, SSO auth domains, and a pair of tokenizers that
minted and stored its own sessions. Hanzo IAM answers all of it, once,
at the edge. Two credential stores is one too many, and the one nobody
signs into is the one that rots.
What is left is Identity: WHO a request is, resolved from what the edge
asserted or from a service-account key, never issued here. The routes are
not merely unrouted — the handlers, stores, tables and templates behind
them are gone, and anonymous_test.go names each dead address so that
re-exempting one fails even though no route can come back.
The census moves with it: 366 addresses to 329, 353 typed ops to 319, ten
escape hatches to seven. zipdoc_gen.go loses the 34 descriptions whose
addresses left, because zip keys its doc registry by method and path, so
a description outliving its declaration attaches this module's prose to
whatever a host serves at that address. zipdoc_test.go pins that.
Cherry-picked from delete/o11y-identity 89854a68ae.
470 lines
12 KiB
Go
470 lines
12 KiB
Go
package impldashboard
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/hanzoai/o11y/pkg/authz"
|
|
"github.com/hanzoai/o11y/pkg/errors"
|
|
"github.com/hanzoai/o11y/pkg/factory"
|
|
"github.com/hanzoai/o11y/pkg/http/binding"
|
|
"github.com/hanzoai/o11y/pkg/http/render"
|
|
"github.com/hanzoai/o11y/pkg/modules/dashboard"
|
|
"github.com/hanzoai/o11y/pkg/transition"
|
|
"github.com/hanzoai/o11y/pkg/types"
|
|
"github.com/hanzoai/o11y/pkg/types/authtypes"
|
|
"github.com/hanzoai/o11y/pkg/types/coretypes"
|
|
"github.com/hanzoai/o11y/pkg/types/dashboardtypes"
|
|
"github.com/hanzoai/o11y/pkg/valuer"
|
|
)
|
|
|
|
type handler struct {
|
|
module dashboard.Module
|
|
authz authz.AuthZ
|
|
providerSettings factory.ProviderSettings
|
|
}
|
|
|
|
func NewHandler(module dashboard.Module, providerSettings factory.ProviderSettings, authz authz.AuthZ) dashboard.Handler {
|
|
return &handler{module: module, providerSettings: providerSettings, authz: authz}
|
|
}
|
|
|
|
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
req := dashboardtypes.PostableDashboard{}
|
|
err = json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
dashboardMigrator := transition.NewDashboardMigrateV5(handler.providerSettings.Logger, nil, nil)
|
|
if req["version"] != "v5" {
|
|
dashboardMigrator.Migrate(ctx, req)
|
|
}
|
|
|
|
dashboard, err := handler.module.Create(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), dashboardtypes.SourceUser, req)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusCreated, gettableDashboard)
|
|
}
|
|
|
|
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
id := coretypes.Param(r, "id")
|
|
if id == "" {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
|
return
|
|
}
|
|
dashboardID, err := valuer.NewUUID(id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
req := dashboardtypes.UpdatableDashboard{}
|
|
err = json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
diff := 0
|
|
// Allow multiple widget deletions for MACHINE callers; hold a PERSON editing in
|
|
// the console to one at a time. This used to name the identN provider — "is this
|
|
// the tokenizer", i.e. o11y's own session token — which stopped naming a person
|
|
// the moment o11y stopped minting sessions. The distinction it was reaching for
|
|
// is the principal KIND, which every resolver already sets and no deletion can
|
|
// take away.
|
|
if claims.Principal == authtypes.PrincipalUser {
|
|
diff = 1
|
|
}
|
|
|
|
dashboard, err := handler.module.Update(ctx, orgID, dashboardID, claims.Email, req, diff)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, dashboard)
|
|
}
|
|
|
|
func (handler *handler) LockUnlock(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
id := coretypes.Param(r, "id")
|
|
if id == "" {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
|
return
|
|
}
|
|
dashboardID, err := valuer.NewUUID(id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
req := new(dashboardtypes.LockUnlockDashboard)
|
|
err = json.NewDecoder(r.Body).Decode(req)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
isAdmin := false
|
|
selectors := []coretypes.Selector{
|
|
coretypes.TypeRole.MustSelector(authtypes.O11yAdminRoleName),
|
|
}
|
|
err = handler.authz.CheckWithTupleCreation(
|
|
ctx,
|
|
claims,
|
|
valuer.MustNewUUID(claims.OrgID),
|
|
authtypes.Relation{Verb: coretypes.VerbAssignee},
|
|
coretypes.NewResourceRole(),
|
|
selectors,
|
|
selectors,
|
|
)
|
|
if err == nil {
|
|
isAdmin = true
|
|
}
|
|
|
|
err = handler.module.LockUnlock(ctx, orgID, dashboardID, claims.Email, isAdmin, *req.Locked)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, nil)
|
|
|
|
}
|
|
|
|
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
id := coretypes.Param(r, "id")
|
|
if id == "" {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
|
return
|
|
}
|
|
dashboardID, err := valuer.NewUUID(id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
err = handler.module.Delete(ctx, orgID, dashboardID)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (handler *handler) CreatePublic(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
id, err := valuer.NewUUID(coretypes.Param(r, "id"))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
_, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
req := new(dashboardtypes.PostablePublicDashboard)
|
|
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
publicDashboard := dashboardtypes.NewPublicDashboard(req.TimeRangeEnabled, req.DefaultTimeRange, id)
|
|
err = handler.module.CreatePublic(ctx, valuer.MustNewUUID(claims.OrgID), publicDashboard)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusCreated, types.Identifiable{ID: publicDashboard.ID})
|
|
}
|
|
|
|
func (handler *handler) GetPublic(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
id, err := valuer.NewUUID(coretypes.Param(r, "id"))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
_, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
publicDashboard, err := handler.module.GetPublic(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, dashboardtypes.NewGettablePublicDashboard(publicDashboard))
|
|
}
|
|
|
|
func (handler *handler) GetPublicData(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
id, err := valuer.NewUUID(coretypes.Param(r, "id"))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
dashboard, err := handler.module.GetDashboardByPublicID(ctx, id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
publicDashboard, err := handler.module.GetPublic(ctx, dashboard.OrgID, valuer.MustNewUUID(dashboard.ID))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
gettablePublicDashboardData, err := dashboardtypes.NewPublicDashboardDataFromDashboard(dashboard, publicDashboard)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, gettablePublicDashboardData)
|
|
}
|
|
|
|
func (handler *handler) GetPublicWidgetQueryRange(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
id, err := valuer.NewUUID(coretypes.Param(r, "id"))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
widgetIndex := coretypes.Param(r, "idx")
|
|
if widgetIndex == "" {
|
|
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "widget index is missing from the path"))
|
|
return
|
|
}
|
|
|
|
dashboard, err := handler.module.GetDashboardByPublicID(ctx, id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
publicDashboard, err := handler.module.GetPublic(ctx, dashboard.OrgID, valuer.MustNewUUID(dashboard.ID))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
widgetIdx, err := strconv.ParseUint(widgetIndex, 10, 64)
|
|
if err != nil {
|
|
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "invalid widget index"))
|
|
return
|
|
}
|
|
|
|
var startTime, endTime uint64
|
|
if publicDashboard.TimeRangeEnabled {
|
|
startTimeUint, err := strconv.ParseUint(r.URL.Query().Get("startTime"), 10, 64)
|
|
if err != nil {
|
|
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "invalid startTime"))
|
|
return
|
|
}
|
|
|
|
endTimeUint, err := strconv.ParseUint(r.URL.Query().Get("endTime"), 10, 64)
|
|
if err != nil {
|
|
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "invalid endTime"))
|
|
return
|
|
}
|
|
|
|
startTime = startTimeUint
|
|
endTime = endTimeUint
|
|
} else {
|
|
timeRange, err := time.ParseDuration(publicDashboard.DefaultTimeRange)
|
|
if err != nil {
|
|
// this should't happen as we shouldn't let such values in DB
|
|
panic(err)
|
|
}
|
|
|
|
startTime = uint64(time.Now().Add(-timeRange).UnixMilli())
|
|
endTime = uint64(time.Now().UnixMilli())
|
|
}
|
|
|
|
queryRangeResults, err := handler.module.GetPublicWidgetQueryRange(ctx, id, widgetIdx, startTime, endTime)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, queryRangeResults)
|
|
}
|
|
|
|
func (handler *handler) UpdatePublic(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
id, err := valuer.NewUUID(coretypes.Param(r, "id"))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
_, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
req := new(dashboardtypes.UpdatablePublicDashboard)
|
|
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
publicDashboard, err := handler.module.GetPublic(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
publicDashboard.Update(req.TimeRangeEnabled, req.DefaultTimeRange)
|
|
err = handler.module.UpdatePublic(ctx, valuer.MustNewUUID(claims.OrgID), publicDashboard)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (handler *handler) DeletePublic(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
id, err := valuer.NewUUID(coretypes.Param(r, "id"))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
_, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
err = handler.module.DeletePublic(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusNoContent, nil)
|
|
}
|