Files
o11y/pkg/community/community.go
zeekay 69b187636d Let Hanzo IAM answer who the caller is
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.
2026-09-08 23:24:35 -07:00

202 lines
10 KiB
Go

// Package community constructs the ONE Hanzo o11y (O11y-community) runtime and
// HTTP server used by BOTH the standalone `cmd/community` binary AND the unified
// hanzoai/cloud binary's in-process embed (via app.Server.PublicHandler).
//
// Keeping the whole construction — config resolution, the O11y provider set,
// and the app server — behind a single exported builder guarantees the two
// deployments run byte-identical middleware, identity (pkg/identn/iamidentn,
// i.e. Hanzo IAM gateway-header auth), authz (iamauthz), telemetry stores, rule
// manager, dashboards and alerts. One construction, one way: the embed cannot
// drift from the standalone pod's auth or wiring, because they are the same code.
package community
import (
"context"
"log/slog"
"github.com/hanzoai/o11y/pkg/alertmanager"
"github.com/hanzoai/o11y/pkg/analytics"
"github.com/hanzoai/o11y/pkg/auditor"
"github.com/hanzoai/o11y/pkg/authz"
"github.com/hanzoai/o11y/pkg/authz/iamauthz"
"github.com/hanzoai/o11y/pkg/authz/localauthz"
"github.com/hanzoai/o11y/pkg/cache"
"github.com/hanzoai/o11y/pkg/config"
"github.com/hanzoai/o11y/pkg/config/envprovider"
"github.com/hanzoai/o11y/pkg/config/fileprovider"
"github.com/hanzoai/o11y/pkg/factory"
"github.com/hanzoai/o11y/pkg/flagger"
"github.com/hanzoai/o11y/pkg/gateway"
"github.com/hanzoai/o11y/pkg/gateway/noopgateway"
"github.com/hanzoai/o11y/pkg/global"
"github.com/hanzoai/o11y/pkg/licensing"
"github.com/hanzoai/o11y/pkg/licensing/nooplicensing"
"github.com/hanzoai/o11y/pkg/meterreporter"
"github.com/hanzoai/o11y/pkg/modules/cloudintegration"
"github.com/hanzoai/o11y/pkg/modules/cloudintegration/implcloudintegration"
"github.com/hanzoai/o11y/pkg/modules/dashboard"
"github.com/hanzoai/o11y/pkg/modules/dashboard/impldashboard"
"github.com/hanzoai/o11y/pkg/modules/metricreductionrule"
"github.com/hanzoai/o11y/pkg/modules/metricreductionrule/implmetricreductionrule"
"github.com/hanzoai/o11y/pkg/modules/organization"
"github.com/hanzoai/o11y/pkg/modules/retention"
"github.com/hanzoai/o11y/pkg/modules/rulestatehistory"
"github.com/hanzoai/o11y/pkg/modules/serviceaccount"
"github.com/hanzoai/o11y/pkg/modules/tag"
"github.com/hanzoai/o11y/pkg/o11y"
"github.com/hanzoai/o11y/pkg/prometheus"
"github.com/hanzoai/o11y/pkg/querier"
"github.com/hanzoai/o11y/pkg/query-service/app"
"github.com/hanzoai/o11y/pkg/queryparser"
"github.com/hanzoai/o11y/pkg/ruler"
"github.com/hanzoai/o11y/pkg/ruler/o11yruler"
"github.com/hanzoai/o11y/pkg/sqlschema"
"github.com/hanzoai/o11y/pkg/sqlstore"
"github.com/hanzoai/o11y/pkg/telemetrystore"
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
"github.com/hanzoai/o11y/pkg/zeus"
"github.com/hanzoai/o11y/pkg/zeus/noopzeus"
)
// NewConfig resolves the O11y config from the given YAML files (if any) plus
// the process environment (env:), applying the Hanzo operator-facing aliases
// (e.g. the flat O11Y_DATASTORE_DSN → telemetrystore.datastore.dsn) inside
// o11y.NewConfig. This is THE config path; cmd.NewO11yConfig delegates here
// so standalone and embed read configuration identically.
func NewConfig(ctx context.Context, logger *slog.Logger, configFiles []string) (o11y.Config, error) {
uris := make([]string, 0, len(configFiles)+1)
for _, f := range configFiles {
uris = append(uris, "file:"+f)
}
uris = append(uris, "env:")
return o11y.NewConfig(
ctx,
logger,
config.ResolverConfig{
Uris: uris,
ProviderFactories: []config.ProviderFactory{
envprovider.NewFactory(),
fileprovider.NewFactory(),
},
},
)
}
// SQLStoreProviderFactories is the community SQL store provider set (the
// control-plane metadata store — sqlite by default).
func SQLStoreProviderFactories() factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]] {
return o11y.NewSQLStoreProviderFactories()
}
// SQLSchemaProviderFactories is the community SQL schema provider set.
func SQLSchemaProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
return o11y.NewSQLSchemaProviderFactories(sqlstore)
}
// NewO11y constructs the O11y runtime with the community provider set: noop
// zeus/licensing/gateway, Hanzo IAM authz (iamauthz — the sole authorizer), the
// Datastore (Hanzo Datastore) telemetry store, sqlite control-plane store, the
// full dashboard/cloudintegration/metricreductionrule/ruler modules, and (wired
// internally by o11y.New) the identN provider set including iamidentn — the
// gateway-header human identity the running pod trusts. The provider list is the
// single source of truth for how o11y boots.
func NewO11y(ctx context.Context, config o11y.Config) (*o11y.O11y, error) {
return o11y.New(
ctx,
config,
zeus.Config{},
noopzeus.NewProviderFactory(),
licensing.Config{},
func(_ sqlstore.SQLStore, _ zeus.Zeus, _ organization.Getter, _ analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return nooplicensing.NewFactory()
},
o11y.NewEmailingProviderFactories(),
o11y.NewCacheProviderFactories(),
o11y.NewWebProviderFactories(config.Global),
SQLSchemaProviderFactories,
SQLStoreProviderFactories(),
o11y.NewTelemetryStoreProviderFactories(),
func(_ context.Context, sqlstore sqlstore.SQLStore, config authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
// Authorization is selected by the TRUST BOUNDARY, not by pluggable policy —
// the enforced policy is identical either way, only where the relationship
// tuples live differs:
// - "local" (O11Y_AUTHZ_PROVIDER=local): an edge gateway has ALREADY
// validated the Hanzo IAM session and injected the trusted identity
// headers, and the sharder gates cross-org — so authorizing that user
// for their own org is a local decision, and a round-trip back out to
// IAM is redundant with the edge trust already spent. See
// pkg/authz/localauthz.
// - "iam" (default): every decision AND every tuple WRITE is delegated to
// Hanzo IAM's Casbin enforcer.
//
// The boundary is a property of the DEPLOYMENT, not of standalone-vs-embed.
// This comment used to say "iam" was what standalone means and "local" what
// the embed means, which stopped being true the moment a standalone host was
// put behind the same gateway hop: o11y.hanzo.ai is fronted by admin-guard's
// verify-authn, so its boundary is the embed's boundary and it runs "local".
//
// A WARNING the default cannot express: "iam" requires IAM to serve
// add-policy / remove-policy / get-policies / batch-enforce, and IAM v1.34 —
// the clean-room zip v2 — does not. Selecting it today makes the founding
// grant fail with authz_unavailable on every sign-in. Either front the host
// with the gateway and choose "local", or restore those routes first.
if config.Provider == "local" {
return localauthz.NewProviderFactory(sqlstore), nil
}
return iamauthz.NewProviderFactory(sqlstore), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, tagModule)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()
},
func(_ licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
return o11y.NewAuditorProviderFactories()
},
func(_ context.Context, _ factory.ProviderSettings, _ flagger.Flagger, _ licensing.Licensing, _ telemetrystore.TelemetryStore, _ retention.Getter, _ organization.Getter, _ zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string) {
return o11y.NewMeterReporterProviderFactories(), "noop"
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
return querier.NewHandler(ps, q, a)
},
func(_ sqlstore.SQLStore, _ dashboard.Module, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
return implcloudintegration.NewModule(), nil
},
func(_ sqlstore.SQLStore, _ telemetrystore.TelemetryStore, _ dashboard.Module, _ queryparser.QueryParser, _ licensing.Licensing, _ flagger.Flagger, _ telemetrytypes.MetadataStore, _ factory.ProviderSettings, _ int) metricreductionrule.Module {
return implmetricreductionrule.NewModule()
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
return factory.MustNewNamedMap(o11yruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, nil, nil))
},
)
}
// NewServer constructs the O11y runtime and its HTTP server together. Callers
// choose the serving mode:
//
// - standalone (cmd/community): server.Start binds the listeners; O11y.Start
// runs background evaluation; O11y.Wait blocks.
// - embedded (hanzoai/cloud): O11y.Start runs background evaluation, and the
// server's route table is installed via o11y.SetRuntime — cloud's own HTTP
// stack serves /v1/o11y/*, resolving each address against that table; the
// listeners are never bound. A host that runs the runtime as a separate
// Deployment installs o11y.Whole over its proxy instead, because for a
// remote every address really is the same endpoint.
//
// Both paths share this ONE construction, so identity and authz are identical.
func NewServer(ctx context.Context, config o11y.Config) (*app.Server, *o11y.O11y, error) {
sn, err := NewO11y(ctx, config)
if err != nil {
return nil, nil, err
}
server, err := app.NewServer(config, sn)
if err != nil {
return nil, nil, err
}
return server, sn, nil
}