/* 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 persistence import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/edge/eid" "github.com/openziti/fabric/controller/db" "github.com/openziti/foundation/storage/ast" "github.com/openziti/foundation/storage/boltz" "github.com/openziti/foundation/util/errorz" "github.com/openziti/sdk-golang/ziti" "github.com/pkg/errors" "go.etcd.io/bbolt" ) const ( FieldIdentityType = "type" FieldIdentityIsDefaultAdmin = "isDefaultAdmin" FieldIdentityIsAdmin = "isAdmin" FieldIdentityEnrollments = "enrollments" FieldIdentityAuthenticators = "authenticators" FieldIdentityServiceConfigs = "serviceConfigs" FieldIdentityEnvInfoArch = "envInfoArch" FieldIdentityEnvInfoOs = "envInfoOs" FieldIdentityEnvInfoOsRelease = "envInfoRelease" FieldIdentityEnvInfoOsVersion = "envInfoVersion" FieldIdentitySdkInfoBranch = "sdkInfoBranch" FieldIdentitySdkInfoRevision = "sdkInfoRevision" FieldIdentitySdkInfoType = "sdkInfoType" FieldIdentitySdkInfoVersion = "sdkInfoVersion" FieldIdentitySdkInfoAppId = "sdkInfoAppId" FieldIdentitySdkInfoAppVersion = "sdkInfoAppVersion" FieldIdentityBindServices = "bindServices" FieldIdentityDialServices = "dialServices" FieldIdentityDefaultHostingPrecedence = "defaultHostingPrecedence" FieldIdentityDefaultHostingCost = "defaultHostingCost" FieldIdentityServiceHostingPrecedences = "serviceHostingPrecedences" FieldIdentityServiceHostingCosts = "serviceHostingCosts" FieldIdentityAppData = "appData" ) func newIdentity(name string, identityTypeId string, roleAttributes ...string) *Identity { return &Identity{ BaseExtEntity: boltz.BaseExtEntity{Id: eid.New()}, Name: name, IdentityTypeId: identityTypeId, RoleAttributes: roleAttributes, } } type EnvInfo struct { Arch string Os string OsRelease string OsVersion string } type SdkInfo struct { Branch string Revision string Type string Version string AppId string AppVersion string } type Identity struct { boltz.BaseExtEntity Name string IdentityTypeId string IsDefaultAdmin bool IsAdmin bool Enrollments []string Authenticators []string RoleAttributes []string SdkInfo *SdkInfo EnvInfo *EnvInfo DefaultHostingPrecedence ziti.Precedence DefaultHostingCost uint16 ServiceHostingPrecedences map[string]ziti.Precedence ServiceHostingCosts map[string]uint16 AppData map[string]interface{} } type ServiceConfig struct { ServiceId string ConfigId string } var identityFieldMappings = map[string]string{FieldIdentityType: "identityTypeId"} func (entity *Identity) LoadValues(_ boltz.CrudStore, bucket *boltz.TypedBucket) { entity.LoadBaseValues(bucket) entity.Name = bucket.GetStringOrError(FieldName) entity.IdentityTypeId = bucket.GetStringWithDefault(FieldIdentityType, "") entity.IsDefaultAdmin = bucket.GetBoolWithDefault(FieldIdentityIsDefaultAdmin, false) entity.IsAdmin = bucket.GetBoolWithDefault(FieldIdentityIsAdmin, false) entity.Authenticators = bucket.GetStringList(FieldIdentityAuthenticators) entity.Enrollments = bucket.GetStringList(FieldIdentityEnrollments) entity.RoleAttributes = bucket.GetStringList(FieldRoleAttributes) entity.DefaultHostingPrecedence = ziti.Precedence(bucket.GetInt32WithDefault(FieldIdentityDefaultHostingPrecedence, 0)) entity.DefaultHostingCost = uint16(bucket.GetInt32WithDefault(FieldIdentityDefaultHostingCost, 0)) entity.AppData = bucket.GetMap(FieldIdentityAppData) entity.SdkInfo = &SdkInfo{ Branch: bucket.GetStringWithDefault(FieldIdentitySdkInfoBranch, ""), Revision: bucket.GetStringWithDefault(FieldIdentitySdkInfoRevision, ""), Type: bucket.GetStringWithDefault(FieldIdentitySdkInfoType, ""), Version: bucket.GetStringWithDefault(FieldIdentitySdkInfoVersion, ""), AppId: bucket.GetStringWithDefault(FieldIdentitySdkInfoAppId, ""), AppVersion: bucket.GetStringWithDefault(FieldIdentitySdkInfoAppVersion, ""), } entity.EnvInfo = &EnvInfo{ Arch: bucket.GetStringWithDefault(FieldIdentityEnvInfoArch, ""), Os: bucket.GetStringWithDefault(FieldIdentityEnvInfoOs, ""), OsRelease: bucket.GetStringWithDefault(FieldIdentityEnvInfoOsRelease, ""), OsVersion: bucket.GetStringWithDefault(FieldIdentityEnvInfoOsVersion, ""), } entity.ServiceHostingPrecedences = map[string]ziti.Precedence{} for k, v := range bucket.GetMap(FieldIdentityServiceHostingPrecedences) { entity.ServiceHostingPrecedences[k] = ziti.Precedence(v.(int32)) } entity.ServiceHostingCosts = map[string]uint16{} for k, v := range bucket.GetMap(FieldIdentityServiceHostingCosts) { entity.ServiceHostingCosts[k] = uint16(v.(int32)) } } func (entity *Identity) SetValues(ctx *boltz.PersistContext) { ctx.WithFieldOverrides(identityFieldMappings) entity.SetBaseValues(ctx) store := ctx.Store.(*identityStoreImpl) ctx.SetString(FieldName, entity.Name) ctx.SetBool(FieldIdentityIsDefaultAdmin, entity.IsDefaultAdmin) ctx.SetBool(FieldIdentityIsAdmin, entity.IsAdmin) if oldValue, changed := ctx.GetAndSetString(FieldIdentityType, entity.IdentityTypeId); changed { if oldValue != nil && *oldValue == RouterIdentityType { ctx.Bucket.SetError(errors.New("cannot change type of router identity")) } } store.validateRoleAttributes(entity.RoleAttributes, ctx.Bucket) ctx.SetStringList(FieldRoleAttributes, entity.RoleAttributes) ctx.SetInt32(FieldIdentityDefaultHostingPrecedence, int32(entity.DefaultHostingPrecedence)) ctx.SetInt32(FieldIdentityDefaultHostingCost, int32(entity.DefaultHostingCost)) ctx.Bucket.PutMap(FieldIdentityAppData, entity.AppData, ctx.FieldChecker, false) if entity.EnvInfo != nil { ctx.SetString(FieldIdentityEnvInfoArch, entity.EnvInfo.Arch) ctx.SetString(FieldIdentityEnvInfoOs, entity.EnvInfo.Os) ctx.SetString(FieldIdentityEnvInfoOsRelease, entity.EnvInfo.OsRelease) ctx.SetString(FieldIdentityEnvInfoOsVersion, entity.EnvInfo.OsVersion) } if entity.SdkInfo != nil { ctx.SetString(FieldIdentitySdkInfoBranch, entity.SdkInfo.Branch) ctx.SetString(FieldIdentitySdkInfoRevision, entity.SdkInfo.Revision) ctx.SetString(FieldIdentitySdkInfoType, entity.SdkInfo.Type) ctx.SetString(FieldIdentitySdkInfoVersion, entity.SdkInfo.Version) ctx.SetString(FieldIdentitySdkInfoAppId, entity.SdkInfo.AppId) ctx.SetString(FieldIdentitySdkInfoAppVersion, entity.SdkInfo.AppVersion) } serviceStore := ctx.Store.(*identityStoreImpl).stores.Service if ctx.ProceedWithSet(FieldIdentityServiceHostingPrecedences) { mapBucket, err := ctx.Bucket.EmptyBucket(FieldIdentityServiceHostingPrecedences) if !ctx.Bucket.SetError(err) { for k, v := range entity.ServiceHostingPrecedences { if !serviceStore.IsEntityPresent(ctx.Tx(), k) { ctx.Bucket.SetError(boltz.NewNotFoundError(serviceStore.GetEntityType(), "id", k)) return } mapBucket.SetInt32(k, int32(v), nil) } ctx.Bucket.SetError(mapBucket.Err) } } if ctx.ProceedWithSet(FieldIdentityServiceHostingCosts) { mapBucket, err := ctx.Bucket.EmptyBucket(FieldIdentityServiceHostingCosts) if !ctx.Bucket.SetError(err) { for k, v := range entity.ServiceHostingCosts { if !serviceStore.IsEntityPresent(ctx.Tx(), k) { ctx.Bucket.SetError(boltz.NewNotFoundError(serviceStore.GetEntityType(), "id", k)) return } mapBucket.SetInt32(k, int32(v), nil) } ctx.Bucket.SetError(mapBucket.Err) } } // index change won't fire if we don't have any roles on create, but we need to evaluate if we match any #all roles if ctx.IsCreate && len(entity.RoleAttributes) == 0 { store.rolesChanged(ctx.MutateContext, []byte(entity.Id), nil, nil, ctx.Bucket) } } func (entity *Identity) GetEntityType() string { return EntityTypeIdentities } func (entity *Identity) GetName() string { return entity.Name } type IdentityStore interface { NameIndexedStore LoadOneById(tx *bbolt.Tx, id string) (*Identity, error) LoadOneByName(tx *bbolt.Tx, id string) (*Identity, error) GetRoleAttributesIndex() boltz.SetReadIndex GetRoleAttributesCursorProvider(values []string, semantic string) (ast.SetCursorProvider, error) AssignServiceConfigs(tx *bbolt.Tx, identityId string, serviceConfigs ...ServiceConfig) error RemoveServiceConfigs(tx *bbolt.Tx, identityId string, serviceConfigs ...ServiceConfig) error GetServiceConfigs(tx *bbolt.Tx, identityId string) ([]ServiceConfig, error) LoadServiceConfigsByServiceAndType(tx *bbolt.Tx, identityId string, configTypes map[string]struct{}) map[string]map[string]map[string]interface{} } func newIdentityStore(stores *stores) *identityStoreImpl { store := &identityStoreImpl{ baseStore: newBaseStore(stores, EntityTypeIdentities), } store.InitImpl(store) return store } type identityStoreImpl struct { *baseStore indexName boltz.ReadIndex indexRoleAttributes boltz.SetReadIndex symbolRoleAttributes boltz.EntitySetSymbol symbolAuthenticators boltz.EntitySetSymbol symbolIdentityTypeId boltz.EntitySymbol symbolEnrollments boltz.EntitySetSymbol symbolEdgeRouterPolicies boltz.EntitySetSymbol symbolServicePolicies boltz.EntitySetSymbol symbolEdgeRouters boltz.EntitySetSymbol symbolBindServices boltz.EntitySetSymbol symbolDialServices boltz.EntitySetSymbol edgeRoutersCollection boltz.RefCountedLinkCollection bindServicesCollection boltz.RefCountedLinkCollection dialServicesCollection boltz.RefCountedLinkCollection } func (store *identityStoreImpl) NewStoreEntity() boltz.Entity { return &Identity{} } func (store *identityStoreImpl) GetRoleAttributesIndex() boltz.SetReadIndex { return store.indexRoleAttributes } func (store *identityStoreImpl) initializeLocal() { store.AddExtEntitySymbols() store.symbolRoleAttributes = store.AddPublicSetSymbol(FieldRoleAttributes, ast.NodeTypeString) store.indexRoleAttributes = store.AddSetIndex(store.symbolRoleAttributes) store.indexName = store.addUniqueNameField() store.symbolEdgeRouters = store.AddFkSetSymbol(db.EntityTypeRouters, store.stores.edgeRouter) store.symbolBindServices = store.AddFkSetSymbol(FieldIdentityBindServices, store.stores.edgeService) store.symbolDialServices = store.AddFkSetSymbol(FieldIdentityDialServices, store.stores.edgeService) store.symbolEdgeRouterPolicies = store.AddFkSetSymbol(EntityTypeEdgeRouterPolicies, store.stores.edgeRouterPolicy) store.symbolServicePolicies = store.AddFkSetSymbol(EntityTypeServicePolicies, store.stores.servicePolicy) store.symbolEnrollments = store.AddFkSetSymbol(FieldIdentityEnrollments, store.stores.enrollment) store.symbolAuthenticators = store.AddFkSetSymbol(FieldIdentityAuthenticators, store.stores.authenticator) store.symbolIdentityTypeId = store.AddFkSymbol(FieldIdentityType, store.stores.identityType) store.AddSymbol(FieldIdentityIsAdmin, ast.NodeTypeBool) store.AddSymbol(FieldIdentityIsDefaultAdmin, ast.NodeTypeBool) store.indexRoleAttributes.AddListener(store.rolesChanged) } func (store *identityStoreImpl) initializeLinked() { store.AddLinkCollection(store.symbolEdgeRouterPolicies, store.stores.edgeRouterPolicy.symbolIdentities) store.AddLinkCollection(store.symbolServicePolicies, store.stores.servicePolicy.symbolIdentities) store.edgeRoutersCollection = store.AddRefCountedLinkCollection(store.symbolEdgeRouters, store.stores.edgeRouter.symbolIdentities) store.bindServicesCollection = store.AddRefCountedLinkCollection(store.symbolBindServices, store.stores.edgeService.symbolBindIdentities) store.dialServicesCollection = store.AddRefCountedLinkCollection(store.symbolDialServices, store.stores.edgeService.symbolDialIdentities) } func (store *identityStoreImpl) rolesChanged(mutateCtx boltz.MutateContext, rowId []byte, _ []boltz.FieldTypeAndValue, new []boltz.FieldTypeAndValue, holder errorz.ErrorHolder) { ctx := &roleAttributeChangeContext{ tx: mutateCtx.Tx(), rolesSymbol: store.stores.edgeRouterPolicy.symbolIdentityRoles, linkCollection: store.stores.edgeRouterPolicy.identityCollection, relatedLinkCollection: store.stores.edgeRouterPolicy.edgeRouterCollection, denormLinkCollection: store.edgeRoutersCollection, ErrorHolder: holder, } UpdateRelatedRoles(ctx, rowId, new, store.stores.edgeRouterPolicy.symbolSemantic) ctx = &roleAttributeChangeContext{ tx: mutateCtx.Tx(), rolesSymbol: store.stores.servicePolicy.symbolIdentityRoles, linkCollection: store.stores.servicePolicy.identityCollection, relatedLinkCollection: store.stores.servicePolicy.serviceCollection, ErrorHolder: holder, } store.updateServicePolicyRelatedRoles(ctx, rowId, new) } func (store *identityStoreImpl) GetNameIndex() boltz.ReadIndex { return store.indexName } func (store *identityStoreImpl) LoadOneById(tx *bbolt.Tx, id string) (*Identity, error) { entity := &Identity{} if err := store.baseLoadOneById(tx, id, entity); err != nil { return nil, err } return entity, nil } func (store *identityStoreImpl) LoadOneByName(tx *bbolt.Tx, name string) (*Identity, error) { id := store.indexName.Read(tx, []byte(name)) if id != nil { return store.LoadOneById(tx, string(id)) } return nil, nil } func (store *identityStoreImpl) DeleteById(ctx boltz.MutateContext, id string) error { for _, enrollmentId := range store.GetRelatedEntitiesIdList(ctx.Tx(), id, FieldIdentityEnrollments) { if err := store.stores.enrollment.DeleteById(ctx, enrollmentId); err != nil { return err } } for _, authenticatorId := range store.GetRelatedEntitiesIdList(ctx.Tx(), id, FieldIdentityAuthenticators) { if err := store.stores.authenticator.DeleteById(ctx, authenticatorId); err != nil { return err } } if entity, _ := store.LoadOneById(ctx.Tx(), id); entity != nil { if entity.IdentityTypeId == RouterIdentityType { if !ctx.IsSystemContext() { if router, _ := store.stores.Router.LoadOneByName(ctx.Tx(), entity.Name); router != nil { err := errors.Errorf("cannot delete router identity %v until associated router is deleted", entity.Name) return errorz.NewEntityCanNotBeDeletedFrom(err) } } } // Remove entity from IdentityRoles in edge router policies if err := store.deleteEntityReferences(ctx.Tx(), entity, store.stores.edgeRouterPolicy.symbolIdentityRoles); err != nil { return err } // Remove entity from IdentityRoles in service policies if err := store.deleteEntityReferences(ctx.Tx(), entity, store.stores.servicePolicy.symbolIdentityRoles); err != nil { return err } } if err := store.removeServiceConfigs(ctx.Tx(), id, func(_, _, _ string) bool { return true }); err != nil { return err } return store.baseStore.DeleteById(ctx, id) } func (store *identityStoreImpl) AssignServiceConfigs(tx *bbolt.Tx, identityId string, serviceConfigs ...ServiceConfig) error { entityBucket := store.GetEntityBucket(tx, []byte(identityId)) if entityBucket == nil { return boltz.NewNotFoundError(store.GetSingularEntityType(), "id", identityId) } configsBucket := entityBucket.GetOrCreateBucket(FieldIdentityServiceConfigs) if configsBucket.HasError() { return configsBucket.GetError() } configTypes := map[string]struct{}{} for _, serviceConfig := range serviceConfigs { config, err := store.stores.config.LoadOneById(tx, serviceConfig.ConfigId) if err != nil { return err } if _, ok := configTypes[config.Type]; ok { return errors.Errorf("multiple service configs provided for identity %v of config type %v", identityId, config.Type) } configTypes[config.Type] = struct{}{} serviceBucket := configsBucket.GetOrCreateBucket(serviceConfig.ServiceId) if serviceBucket.HasError() { return serviceBucket.GetError() } // un-index old value if currentConfigId := serviceBucket.GetString(config.Type); currentConfigId != nil { if err := store.stores.config.identityServicesLinks.RemoveCompoundLink(tx, *currentConfigId, ss(identityId, serviceConfig.ServiceId)); err != nil { return err } } // set new value if serviceBucket.SetString(config.Type, config.Id, nil).HasError() { return serviceBucket.GetError() } // index new value if err := store.stores.config.identityServicesLinks.AddCompoundLink(tx, config.Id, ss(identityId, serviceConfig.ServiceId)); err != nil { return err } } var serviceEvents []*ServiceEvent for _, serviceConfig := range serviceConfigs { serviceEvents = append(serviceEvents, &ServiceEvent{ Type: ServiceUpdated, IdentityId: identityId, ServiceId: serviceConfig.ServiceId, }) } tx.OnCommit(func() { ServiceEvents.dispatchEventsAsync(serviceEvents) }) return nil } func (store *identityStoreImpl) RemoveServiceConfigs(tx *bbolt.Tx, identityId string, serviceConfigs ...ServiceConfig) error { // could make this more efficient with maps, but only necessary if we have large number of overrides, which seems unlikely // optimize later, if necessary result := store.removeServiceConfigs(tx, identityId, func(serviceId, _, configId string) bool { if len(serviceConfigs) == 0 { return true } for _, config := range serviceConfigs { if config.ServiceId == serviceId && config.ConfigId == configId { return true } } return false }) var serviceEvents []*ServiceEvent for _, serviceConfig := range serviceConfigs { serviceEvents = append(serviceEvents, &ServiceEvent{ Type: ServiceUpdated, IdentityId: identityId, ServiceId: serviceConfig.ServiceId, }) } tx.OnCommit(func() { ServiceEvents.dispatchEventsAsync(serviceEvents) }) return result } func (store *identityStoreImpl) removeServiceConfigs(tx *bbolt.Tx, identityId string, removeFilter func(serviceId, configTypeId, configId string) bool) error { entityBucket := store.GetEntityBucket(tx, []byte(identityId)) if entityBucket == nil { return boltz.NewNotFoundError(store.GetSingularEntityType(), "id", identityId) } configsBucket := entityBucket.GetBucket(FieldIdentityServiceConfigs) if configsBucket == nil { // no service configs found, nothing to do, bail out return nil } servicesCursor := configsBucket.Cursor() for sKey, _ := servicesCursor.First(); sKey != nil; sKey, _ = servicesCursor.Next() { serviceId := string(sKey) serviceBucket := configsBucket.GetBucket(serviceId) if serviceBucket == nil { // doesn't exist, nothing to do, continue continue } cursor := serviceBucket.Cursor() for k, v := cursor.First(); k != nil; k, v = cursor.Next() { configTypeId := string(k) configId := *boltz.FieldToString(boltz.GetTypeAndValue(v)) if removeFilter(serviceId, configTypeId, configId) { if err := cursor.Delete(); err != nil { return err } if err := store.stores.config.identityServicesLinks.RemoveCompoundLink(tx, configId, ss(identityId, serviceId)); err != nil { return err } } } } return nil } func (store *identityStoreImpl) GetServiceConfigs(tx *bbolt.Tx, identityId string) ([]ServiceConfig, error) { entityBucket := store.GetEntityBucket(tx, []byte(identityId)) if entityBucket == nil { return nil, boltz.NewNotFoundError(store.GetSingularEntityType(), "id", identityId) } var result []ServiceConfig configsBucket := entityBucket.GetBucket(FieldIdentityServiceConfigs) if configsBucket == nil { // no service configs found, nothing to do, bail out return result, nil } servicesCursor := configsBucket.Cursor() for sKey, _ := servicesCursor.First(); sKey != nil; sKey, _ = servicesCursor.Next() { serviceId := string(sKey) serviceBucket := configsBucket.GetBucket(serviceId) if serviceBucket == nil { // doesn't exist, nothing to do, continue continue } cursor := serviceBucket.Cursor() for k, v := cursor.First(); k != nil; k, v = cursor.Next() { configId := *boltz.FieldToString(boltz.GetTypeAndValue(v)) result = append(result, ServiceConfig{ServiceId: serviceId, ConfigId: configId}) } } return result, nil } func (store *identityStoreImpl) LoadServiceConfigsByServiceAndType(tx *bbolt.Tx, identityId string, configTypes map[string]struct{}) map[string]map[string]map[string]interface{} { if len(configTypes) == 0 { return nil } result := map[string]map[string]map[string]interface{}{} entityBucket := store.GetEntityBucket(tx, []byte(identityId)) if entityBucket == nil { return result } configsBucket := entityBucket.GetBucket(FieldIdentityServiceConfigs) if configsBucket == nil { return result } servicesCursor := configsBucket.Cursor() for sKey, _ := servicesCursor.First(); sKey != nil; sKey, _ = servicesCursor.Next() { serviceId := string(sKey) serviceBucket := configsBucket.GetBucket(serviceId) if serviceBucket == nil { // doesn't exist, nothing to do, continue continue } _, wantAll := configTypes["all"] cursor := serviceBucket.Cursor() for k, v := cursor.First(); k != nil; k, v = cursor.Next() { configTypeId := string(k) configId := *boltz.FieldToString(boltz.GetTypeAndValue(v)) wantsType := wantAll if !wantsType { _, wantsType = configTypes[configTypeId] } if wantsType { if config, _ := store.stores.config.LoadOneById(tx, configId); config != nil { serviceMap, ok := result[serviceId] if !ok { serviceMap = map[string]map[string]interface{}{} result[serviceId] = serviceMap } serviceMap[configTypeId] = config.Data } else { pfxlog.Logger().Errorf("config id %v was referenced by identity %v, but no longer exists", configId, identityId) _ = cursor.Delete() } } } } return result } func (store *identityStoreImpl) GetRoleAttributesCursorProvider(values []string, semantic string) (ast.SetCursorProvider, error) { return store.getRoleAttributesCursorProvider(store.indexRoleAttributes, values, semantic) }