mirror of
https://github.com/openziti/ziti.git
synced 2026-09-11 13:29:03 +00:00
f18ce1b580
- collapses the edge service boltz child store into the unified service store:
EdgeService becomes a type alias for Service, and RoleAttributes, Configs, and
EncryptionRequired become top-level Service fields
- adds an IsFabricOnly discriminator separating pure-fabric ("management")
services from edge services
- adds a forward-only, irreversible migration that moves edge service data out of
the edge child bucket, classifies pre-existing services (fabric -> IsFabricOnly
with encryptionRequired=false; edge -> IsFabricOnly=false), deletes the old edge
bucket and its index, and rebuilds the role-attributes index via CheckIntegrity
- guards every edge-facing path on the unified store against fabric-only services
so they stay invisible to the edge API: by-id Read/ReadByName/Delete, list/query
and the detail lister, ReadForIdentity, identity service-config overrides,
service policy / SERP @id and #all/role denormalization, the association-list
routes (terminators, policies, configs), and the router data-model sync
- rejects edge-surface updates of fabric-only services, and preserves edge fields
on fabric-surface updates of edge services
- marks the fabric-only guards TEMPORARY(fabric-edge-collapse): they exist only to
keep seldom-used management services hidden, and are removed when the fabric/edge
distinction is erased
- adds in-CI migration tests (forward plus a round-trip asserting FK sub-bucket
relocation and forward/reverse refcount preservation), durable fabric-only
behavior tests, and store-level policy-exclusion tests
- adds one-time authentic-migration gate tooling under zititest/migration-test
(create-model.sh, query, verify) and its operator runbook
386 lines
13 KiB
Go
386 lines
13 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/foundation/v2/errorz"
|
|
"github.com/openziti/foundation/v2/rate"
|
|
"github.com/openziti/ziti/v2/controller/change"
|
|
"github.com/openziti/ziti/v2/controller/storage/ast"
|
|
"github.com/openziti/ziti/v2/controller/storage/boltz"
|
|
"go.etcd.io/bbolt"
|
|
"go4.org/sort"
|
|
)
|
|
|
|
func NewStoreDefinition[E boltz.ExtEntity](strategy boltz.EntityStrategy[E]) boltz.StoreDefinition[E] {
|
|
entityType := strategy.NewEntity().GetEntityType()
|
|
return boltz.StoreDefinition[E]{
|
|
EntityType: entityType,
|
|
EntityStrategy: strategy,
|
|
BasePath: []string{RootBucket},
|
|
EntityNotFoundF: func(id string) error {
|
|
return boltz.NewNotFoundError(boltz.GetSingularEntityType(entityType), "id", id)
|
|
},
|
|
}
|
|
}
|
|
|
|
func (store *Stores) AddCheckable(checkable boltz.Checkable) {
|
|
store.lock.Lock()
|
|
defer store.lock.Unlock()
|
|
store.checkables = append(store.checkables, checkable)
|
|
}
|
|
|
|
func (stores *Stores) GetStoreList() []boltz.Store {
|
|
var result []boltz.Store
|
|
for _, store := range stores.storeMap {
|
|
result = append(result, store)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (stores *Stores) CheckIntegrity(db boltz.Db, ctx context.Context, fix bool, errorHandler func(error, bool)) error {
|
|
if fix {
|
|
changeCtx := boltz.NewMutateContext(ctx)
|
|
return db.Update(changeCtx, func(changeCtx boltz.MutateContext) error {
|
|
return stores.CheckIntegrityInTx(db, changeCtx, fix, errorHandler)
|
|
})
|
|
}
|
|
|
|
return db.View(func(tx *bbolt.Tx) error {
|
|
changeCtx := boltz.NewTxMutateContext(ctx, tx)
|
|
return stores.CheckIntegrityInTx(db, changeCtx, fix, errorHandler)
|
|
})
|
|
}
|
|
|
|
func (stores *Stores) CheckIntegrityInTx(db boltz.Db, ctx boltz.MutateContext, fix bool, errorHandler func(error, bool)) error {
|
|
if fix {
|
|
pfxlog.Logger().Info("creating database snapshot before attempting to fix data integrity issues")
|
|
if _, _, err := db.SnapshotInTx(ctx.Tx(), db.GetDefaultSnapshotPath()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, checkable := range stores.checkables {
|
|
pfxlog.Logger().Infof("checking integrity of db store: %T", checkable)
|
|
if err := checkable.CheckIntegrity(ctx, fix, errorHandler); err != nil {
|
|
pfxlog.Logger().WithError(err).Infof("error checking integrity of db store: %T", checkable)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Stores struct {
|
|
EventualEventer EventualEventer
|
|
internal *stores
|
|
|
|
Router RouterStore
|
|
Service ServiceStore
|
|
Terminator TerminatorStore
|
|
ApiSession ApiSessionStore
|
|
ApiSessionCertificate ApiSessionCertificateStore
|
|
AuthPolicy AuthPolicyStore
|
|
EventualEvent EventualEventStore
|
|
ExternalJwtSigner ExternalJwtSignerStore
|
|
Ca CaStore
|
|
Config ConfigStore
|
|
ConfigType ConfigTypeStore
|
|
Controller ControllerStore
|
|
EdgeRouter EdgeRouterStore
|
|
EdgeRouterPolicy EdgeRouterPolicyStore
|
|
Identity IdentityStore
|
|
IdentityType IdentityTypeStore
|
|
Index boltz.Store
|
|
Session SessionStore
|
|
Revocation RevocationStore
|
|
ServiceEdgeRouterPolicy ServiceEdgeRouterPolicyStore
|
|
ServicePolicy ServicePolicyStore
|
|
TransitRouter TransitRouterStore
|
|
Enrollment EnrollmentStore
|
|
Authenticator AuthenticatorStore
|
|
PostureCheck PostureCheckStore
|
|
PostureCheckType PostureCheckTypeStore
|
|
Mfa MfaStore
|
|
storeMap map[reflect.Type]boltz.Store
|
|
lock sync.Mutex
|
|
checkables []boltz.Checkable
|
|
}
|
|
|
|
func (stores *Stores) buildStoreMap() {
|
|
stores.storeMap = map[reflect.Type]boltz.Store{}
|
|
val := reflect.ValueOf(stores).Elem()
|
|
for i := 0; i < val.NumField(); i++ {
|
|
f := val.Field(i)
|
|
if f.CanInterface() {
|
|
if store, ok := f.Interface().(boltz.Store); ok && store.GetEntityType() != "indexes" {
|
|
entityType := store.GetEntityReflectType()
|
|
stores.storeMap[entityType] = store
|
|
stores.AddCheckable(store)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (stores *Stores) GetEntityCounts(db boltz.Db) (map[string]int64, error) {
|
|
result := map[string]int64{}
|
|
for _, store := range stores.storeMap {
|
|
err := db.View(func(tx *bbolt.Tx) error {
|
|
key := store.GetEntityType()
|
|
if store.IsChildStore() {
|
|
if _, ok := store.(TransitRouterStore); ok {
|
|
// skip transit routers, since count will be == fabric routers
|
|
return nil
|
|
} else {
|
|
key = store.GetEntityType() + ".edge"
|
|
}
|
|
}
|
|
|
|
_, count, err := store.QueryIds(tx, "true limit 1")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result[key] = count
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (stores *Stores) getStoresForInit() []initializableStore {
|
|
var result []initializableStore
|
|
for _, crudStore := range stores.storeMap {
|
|
if store, ok := crudStore.(initializableStore); ok {
|
|
result = append(result, store)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (stores *Stores) GetStoreForEntity(entity boltz.Entity) boltz.Store {
|
|
key := reflect.TypeOf(entity)
|
|
return stores.storeMap[key]
|
|
}
|
|
|
|
func (stores *Stores) GetStores() []boltz.Store {
|
|
var result []boltz.Store
|
|
for _, store := range stores.storeMap {
|
|
result = append(result, store)
|
|
}
|
|
return result
|
|
}
|
|
|
|
type stores struct {
|
|
EventualEventer EventualEventer
|
|
|
|
terminator *terminatorStoreImpl
|
|
router *routerStoreImpl
|
|
service *serviceStoreImpl
|
|
apiSession *apiSessionStoreImpl
|
|
authPolicy *AuthPolicyStoreImpl
|
|
eventualEvent *eventualEventStoreImpl
|
|
ca *caStoreImpl
|
|
config *configStoreImpl
|
|
configType *configTypeStoreImpl
|
|
controller *controllerStoreImpl
|
|
edgeRouter *edgeRouterStoreImpl
|
|
edgeRouterPolicy *edgeRouterPolicyStoreImpl
|
|
externalJwtSigner *externalJwtSignerStoreImpl
|
|
identity *identityStoreImpl
|
|
identityType *IdentityTypeStoreImpl
|
|
revocation *revocationStoreImpl
|
|
serviceEdgeRouterPolicy *serviceEdgeRouterPolicyStoreImpl
|
|
servicePolicy *servicePolicyStoreImpl
|
|
session *sessionStoreImpl
|
|
transitRouter *transitRouterStoreImpl
|
|
enrollment *enrollmentStoreImpl
|
|
authenticator *authenticatorStoreImpl
|
|
postureCheck *postureCheckStoreImpl
|
|
postureCheckType *postureCheckTypeStoreImpl
|
|
apiSessionCertificate *ApiSessionCertificateStoreImpl
|
|
mfa *MfaStoreImpl
|
|
|
|
rateLimiter rate.RateLimiter
|
|
}
|
|
|
|
type DbProvider interface {
|
|
GetDb() boltz.Db
|
|
}
|
|
|
|
type DbProviderF func() boltz.Db
|
|
|
|
func (f DbProviderF) GetDb() boltz.Db {
|
|
return f()
|
|
}
|
|
|
|
func InitStores(db boltz.Db, rateLimiter rate.RateLimiter, signingCert *x509.Certificate) (*Stores, error) {
|
|
dbProvider := DbProviderF(func() boltz.Db {
|
|
return db
|
|
})
|
|
errorHolder := &errorz.ErrorHolderImpl{}
|
|
|
|
internalStores := &stores{
|
|
rateLimiter: rateLimiter,
|
|
}
|
|
|
|
internalStores.eventualEvent = newEventualEventStore(internalStores)
|
|
internalStores.EventualEventer = NewEventualEventerBbolt(dbProvider, internalStores.eventualEvent, 2*time.Second, 1000)
|
|
|
|
internalStores.router = newRouterStore(internalStores)
|
|
internalStores.service = newServiceStore(internalStores)
|
|
internalStores.terminator = newTerminatorStore(internalStores)
|
|
|
|
internalStores.apiSession = newApiSessionStore(internalStores)
|
|
internalStores.apiSessionCertificate = newApiSessionCertificateStore(internalStores)
|
|
internalStores.authenticator = newAuthenticatorStore(internalStores)
|
|
internalStores.authPolicy = newAuthPolicyStore(internalStores)
|
|
internalStores.ca = newCaStore(internalStores)
|
|
internalStores.config = newConfigsStore(internalStores)
|
|
internalStores.configType = newConfigTypesStore(internalStores)
|
|
internalStores.controller = newControllerStore(internalStores)
|
|
internalStores.edgeRouter = newEdgeRouterStore(internalStores)
|
|
internalStores.edgeRouterPolicy = newEdgeRouterPolicyStore(internalStores)
|
|
internalStores.externalJwtSigner = newExternalJwtSignerStore(internalStores)
|
|
internalStores.transitRouter = newTransitRouterStore(internalStores)
|
|
internalStores.identity = newIdentityStore(internalStores)
|
|
internalStores.identityType = newIdentityTypeStore(internalStores)
|
|
internalStores.enrollment = newEnrollmentStore(internalStores)
|
|
internalStores.revocation = newRevocationStore(internalStores)
|
|
internalStores.serviceEdgeRouterPolicy = newServiceEdgeRouterPolicyStore(internalStores)
|
|
internalStores.servicePolicy = newServicePolicyStore(internalStores)
|
|
internalStores.session = newSessionStore(internalStores)
|
|
internalStores.postureCheck = newPostureCheckStore(internalStores)
|
|
internalStores.postureCheckType = newPostureCheckTypeStore(internalStores)
|
|
internalStores.mfa = newMfaStore(internalStores)
|
|
|
|
externalStores := &Stores{
|
|
internal: internalStores,
|
|
|
|
Terminator: internalStores.terminator,
|
|
Router: internalStores.router,
|
|
Service: internalStores.service,
|
|
|
|
ApiSession: internalStores.apiSession,
|
|
ApiSessionCertificate: internalStores.apiSessionCertificate,
|
|
AuthPolicy: internalStores.authPolicy,
|
|
EventualEvent: internalStores.eventualEvent,
|
|
Ca: internalStores.ca,
|
|
Config: internalStores.config,
|
|
ConfigType: internalStores.configType,
|
|
Controller: internalStores.controller,
|
|
EdgeRouter: internalStores.edgeRouter,
|
|
EdgeRouterPolicy: internalStores.edgeRouterPolicy,
|
|
ExternalJwtSigner: internalStores.externalJwtSigner,
|
|
TransitRouter: internalStores.transitRouter,
|
|
Identity: internalStores.identity,
|
|
IdentityType: internalStores.identityType,
|
|
Revocation: internalStores.revocation,
|
|
ServiceEdgeRouterPolicy: internalStores.serviceEdgeRouterPolicy,
|
|
ServicePolicy: internalStores.servicePolicy,
|
|
Session: internalStores.session,
|
|
Authenticator: internalStores.authenticator,
|
|
Enrollment: internalStores.enrollment,
|
|
PostureCheck: internalStores.postureCheck,
|
|
PostureCheckType: internalStores.postureCheckType,
|
|
Mfa: internalStores.mfa,
|
|
|
|
storeMap: make(map[reflect.Type]boltz.Store),
|
|
}
|
|
|
|
externalStores.EventualEventer = internalStores.EventualEventer
|
|
|
|
// The Index store is used for querying indexes. It's a convenient store with only a single value (id), which
|
|
// is only ever queried using an index set cursor
|
|
indexStoreDef := boltz.StoreDefinition[boltz.ExtEntity]{
|
|
EntityType: "indexes",
|
|
BasePath: []string{RootBucket},
|
|
EntityNotFoundF: func(id string) error {
|
|
panic(errors.New("programming error"))
|
|
},
|
|
}
|
|
|
|
indexStore := boltz.NewBaseStore(indexStoreDef)
|
|
indexStore.AddIdSymbol("id", ast.NodeTypeString)
|
|
|
|
externalStores.Index = indexStore
|
|
|
|
externalStores.buildStoreMap()
|
|
storeList := externalStores.getStoresForInit()
|
|
|
|
sort.Slice(storeList, func(i, j int) bool {
|
|
if storeList[i].IsChildStore() == storeList[j].IsChildStore() {
|
|
return storeList[i].GetEntityType() < storeList[j].GetEntityType()
|
|
}
|
|
return !storeList[i].IsChildStore()
|
|
})
|
|
|
|
mutateCtx := change.New().SetSourceType("system.initialization").SetChangeAuthorType(change.AuthorTypeController).NewMutateContext()
|
|
err := dbProvider.GetDb().Update(mutateCtx, func(ctx boltz.MutateContext) error {
|
|
for _, store := range storeList {
|
|
store.initializeLocal()
|
|
}
|
|
for _, store := range storeList {
|
|
store.initializeLinked()
|
|
}
|
|
for _, store := range storeList {
|
|
store.initializeIndexes(ctx.Tx(), errorHolder)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
errorHolder.SetError(err)
|
|
if errorHolder.HasError() {
|
|
return nil, errorHolder.GetError()
|
|
}
|
|
|
|
if err = RunMigrations(db, externalStores, signingCert); err != nil {
|
|
return nil, errorz.NewUnhandled(fmt.Errorf("edge datastore migrations failed: %w", err))
|
|
}
|
|
|
|
return externalStores, nil
|
|
}
|
|
|
|
func newBaseStore[E boltz.ExtEntity](stores *stores, strategy boltz.EntityStrategy[E]) *baseStore[E] {
|
|
return &baseStore[E]{
|
|
stores: stores,
|
|
BaseStore: boltz.NewBaseStore(NewStoreDefinition[E](strategy)),
|
|
}
|
|
}
|
|
|
|
func newChildBaseStore[E boltz.ExtEntity](stores *stores, parentMapper func(entity boltz.Entity) boltz.Entity, strategy boltz.EntityStrategy[E], parent boltz.Store, path string) *baseStore[E] {
|
|
def := NewStoreDefinition[E](strategy)
|
|
def.BasePath = []string{path}
|
|
def.Parent = parent
|
|
def.ParentMapper = parentMapper
|
|
return &baseStore[E]{
|
|
stores: stores,
|
|
BaseStore: boltz.NewBaseStore[E](def),
|
|
}
|
|
}
|